D47crunch
Standardization and analytical error propagation of Δ47 and Δ48 clumped-isotope measurements
Process and standardize carbonate and/or CO2 clumped-isotope analyses, from low-level data out of a dual-inlet mass spectrometer to final, “absolute” Δ47 and Δ48 values with fully propagated analytical error estimates (Daëron, 2021).
The tutorial section takes you through a series of simple steps to import/process data and print out the results. The how-to section provides instructions applicable to various specific tasks.
1. Tutorial
1.1 Installation
The easy option is to use pip; open a shell terminal and simply type:
python -m pip install D47crunch
For those wishing to experiment with the bleeding-edge development version, this can be done through the following steps:
- Download the
devbranch source code here and rename it toD47crunch.py. - Do any of the following:
- copy
D47crunch.pyto somewhere in your Python path - copy
D47crunch.pyto a working directory (import D47crunchwill only work if called within that directory) - copy
D47crunch.pyto any other location (e.g.,/foo/bar) and then use the following code snippet in your own code to importD47crunch:
- copy
import sys
sys.path.append('/foo/bar')
import D47crunch
Documentation for the development version can be downloaded here (save html file and open it locally).
1.2 Usage
Start by creating a file named rawdata.csv with the following contents:
UID, Sample, d45, d46, d47, d48, d49
A01, ETH-1, 5.79502, 11.62767, 16.89351, 24.56708, 0.79486
A02, MYSAMPLE-1, 6.21907, 11.49107, 17.27749, 24.58270, 1.56318
A03, ETH-2, -6.05868, -4.81718, -11.63506, -10.32578, 0.61352
A04, MYSAMPLE-2, -3.86184, 4.94184, 0.60612, 10.52732, 0.57118
A05, ETH-3, 5.54365, 12.05228, 17.40555, 25.96919, 0.74608
A06, ETH-2, -6.06706, -4.87710, -11.69927, -10.64421, 1.61234
A07, ETH-1, 5.78821, 11.55910, 16.80191, 24.56423, 1.47963
A08, MYSAMPLE-2, -3.87692, 4.86889, 0.52185, 10.40390, 1.07032
Then instantiate a D47data object which will store and process this data:
import D47crunch
mydata = D47crunch.D47data()
For now, this object is empty:
>>> print(mydata)
[]
To load the analyses saved in rawdata.csv into our D47data object and process the data:
mydata.read('rawdata.csv')
# compute δ13C, δ18O of working gas:
mydata.wg()
# compute δ13C, δ18O, raw Δ47 values for each analysis:
mydata.crunch()
# compute absolute Δ47 values for each analysis
# as well as average Δ47 values for each sample:
mydata.standardize()
We can now print a summary of the data processing:
>>> mydata.summary(verbose = True, save_to_file = False)
[summary]
––––––––––––––––––––––––––––––– –––––––––
N samples (anchors + unknowns) 5 (3 + 2)
N analyses (anchors + unknowns) 8 (5 + 3)
Repeatability of δ13C_VPDB 4.2 ppm
Repeatability of δ18O_VSMOW 47.5 ppm
Repeatability of Δ47 (anchors) 13.4 ppm
Repeatability of Δ47 (unknowns) 2.5 ppm
Repeatability of Δ47 (all) 9.6 ppm
Model degrees of freedom 3
Student's 95% t-factor 3.18
Standardization method pooled
––––––––––––––––––––––––––––––– –––––––––
This tells us that our data set contains 5 different samples: 3 anchors (ETH-1, ETH-2, ETH-3) and 2 unknowns (MYSAMPLE-1, MYSAMPLE-2). The total number of analyses is 8, with 5 anchor analyses and 3 unknown analyses. We get an estimate of the analytical repeatability (i.e. the overall, pooled standard deviation) for δ13C, δ18O and Δ47, as well as the number of degrees of freedom (here, 3) that these estimated standard deviations are based on, along with the corresponding Student's t-factor (here, 3.18) for 95 % confidence limits. Finally, the summary indicates that we used a “pooled” standardization approach (see [Daëron, 2021]).
To see the actual results:
>>> mydata.table_of_samples(verbose = True, save_to_file = False)
[table_of_samples]
–––––––––– – ––––––––– –––––––––– –––––– –––––– –––––––– –––––– ––––––––
Sample N d13C_VPDB d18O_VSMOW D47 SE 95% CL SD p_Levene
–––––––––– – ––––––––– –––––––––– –––––– –––––– –––––––– –––––– ––––––––
ETH-1 2 2.01 37.01 0.2052 0.0131
ETH-2 2 -10.17 19.88 0.2085 0.0026
ETH-3 1 1.73 37.49 0.6132
MYSAMPLE-1 1 2.48 36.90 0.2996 0.0091 ± 0.0291
MYSAMPLE-2 2 -8.17 30.05 0.6600 0.0115 ± 0.0366 0.0025
–––––––––– – ––––––––– –––––––––– –––––– –––––– –––––––– –––––– ––––––––
This table lists, for each sample, the number of analytical replicates, average δ13C and δ18O values (for the analyte CO2 , not for the carbonate itself), the average Δ47 value and the SD of Δ47 for all replicates of this sample. For unknown samples, the SE and 95 % confidence limits for mean Δ47 are also listed These 95 % CL take into account the number of degrees of freedom of the regression model, so that in large datasets the 95 % CL will tend to 1.96 times the SE, but in this case the applicable t-factor is much larger.
We can also generate a table of all analyses in the data set (again, note that d18O_VSMOW is the composition of the CO2 analyte):
>>> mydata.table_of_analyses(verbose = True, save_to_file = False)
[table_of_analyses]
––– ––––––––– –––––––––– ––––––––––– –––––––––––– ––––––––– ––––––––– –––––––––– –––––––––– –––––––– –––––––––– –––––––––– ––––––––– ––––––––– –––––––––– ––––––––
UID Session Sample d13Cwg_VPDB d18Owg_VSMOW d45 d46 d47 d48 d49 d13C_VPDB d18O_VSMOW D47raw D48raw D49raw D47
––– ––––––––– –––––––––– ––––––––––– –––––––––––– ––––––––– ––––––––– –––––––––– –––––––––– –––––––– –––––––––– –––––––––– ––––––––– ––––––––– –––––––––– ––––––––
A01 mySession ETH-1 -3.807 24.921 5.795020 11.627670 16.893510 24.567080 0.794860 2.014086 37.041843 -0.574686 1.149684 -27.690250 0.214454
A02 mySession MYSAMPLE-1 -3.807 24.921 6.219070 11.491070 17.277490 24.582700 1.563180 2.476827 36.898281 -0.499264 1.435380 -27.122614 0.299589
A03 mySession ETH-2 -3.807 24.921 -6.058680 -4.817180 -11.635060 -10.325780 0.613520 -10.166796 19.907706 -0.685979 -0.721617 16.716901 0.206693
A04 mySession MYSAMPLE-2 -3.807 24.921 -3.861840 4.941840 0.606120 10.527320 0.571180 -8.159927 30.087230 -0.248531 0.613099 -4.979413 0.658270
A05 mySession ETH-3 -3.807 24.921 5.543650 12.052280 17.405550 25.969190 0.746080 1.727029 37.485567 -0.226150 1.678699 -28.280301 0.613200
A06 mySession ETH-2 -3.807 24.921 -6.067060 -4.877100 -11.699270 -10.644210 1.612340 -10.173599 19.845192 -0.683054 -0.922832 17.861363 0.210328
A07 mySession ETH-1 -3.807 24.921 5.788210 11.559100 16.801910 24.564230 1.479630 2.009281 36.970298 -0.591129 1.282632 -26.888335 0.195926
A08 mySession MYSAMPLE-2 -3.807 24.921 -3.876920 4.868890 0.521850 10.403900 1.070320 -8.173486 30.011134 -0.245768 0.636159 -4.324964 0.661803
––– ––––––––– –––––––––– ––––––––––– –––––––––––– ––––––––– ––––––––– –––––––––– –––––––––– –––––––– –––––––––– –––––––––– ––––––––– ––––––––– –––––––––– ––––––––
2. How-to
2.1 Simulate a virtual data set to play with
It is sometimes convenient to quickly build a virtual data set of analyses, for instance to assess the final analytical precision achievable for a given combination of anchor and unknown analyses (see also Fig. 6 of Daëron, 2021).
This can be achieved with virtual_data(). The example below creates a dataset with four sessions, each of which comprises four analyses of anchor ETH-1, five of ETH-2, six of ETH-3, and two analyses of an unknown sample named FOO with an arbitrarily defined isotopic composition. Analytical repeatabilities for Δ47 and Δ48 are also specified arbitrarily. See the virtual_data() documentation for additional configuration parameters.
from D47crunch import *
args = dict(
samples = [
dict(Sample = 'ETH-1', N = 4),
dict(Sample = 'ETH-2', N = 5),
dict(Sample = 'ETH-3', N = 6),
dict(
Sample = 'FOO',
N = 2,
d13C_VPDB = -5.,
d18O_VPDB = -10.,
D47 = 0.3,
D48 = 0.15
),
],
rD47 = 0.010,
rD48 = 0.030,
)
session1 = virtual_data(session = 'Session_01', **args)
session2 = virtual_data(session = 'Session_02', **args)
session3 = virtual_data(session = 'Session_03', **args)
session4 = virtual_data(session = 'Session_04', **args)
D = D47data(session1 + session2 + session3 + session4)
D.crunch()
D.standardize()
D.table_of_sessions(verbose = True, save_to_file = False)
D.table_of_samples(verbose = True, save_to_file = False)
D.table_of_analyses(verbose = True, save_to_file = False)
2.2 Control data quality
D47crunch offers several tools to visualize processed data. The examples below use the same virtual data set, generated with:
from D47crunch import *
from random import shuffle
# generate virtual data:
args = dict(
samples = [
dict(Sample = 'ETH-1', N = 8),
dict(Sample = 'ETH-2', N = 8),
dict(Sample = 'ETH-3', N = 8),
dict(Sample = 'FOO', N = 4,
d13C_VPDB = -5., d18O_VPDB = -10.,
D47 = 0.3, D48 = 0.15),
dict(Sample = 'BAR', N = 4,
d13C_VPDB = -15., d18O_VPDB = -15.,
D47 = 0.5, D48 = 0.2),
])
sessions = [
virtual_data(session = f'Session_{k+1:02.0f}', seed = int('1234567890'[:k+1]), **args)
for k in range(10)]
# shuffle the data:
data = [r for s in sessions for r in s]
shuffle(data)
data = sorted(data, key = lambda r: r['Session'])
# create D47data instance:
data47 = D47data(data)
# process D47data instance:
data47.crunch()
data47.standardize()
2.2.1 Plotting the distribution of analyses through time
data47.plot_distribution_of_analyses(filename = 'time_distribution.pdf')

The plot above shows the succession of analyses as if they were all distributed at regular time intervals. See D4xdata.plot_distribution_of_analyses() for how to plot analyses as a function of “true” time (based on the TimeTag for each analysis).
2.2.2 Generating session plots
data47.plot_sessions()
Below is one of the resulting sessions plots. Each cross marker is an analysis. Anchors are in red and unknowns in blue. Short horizontal lines show the nominal Δ47 value for anchors, in red, or the average Δ47 value for unknowns, in blue (overall average for all sessions). Curved grey contours correspond to Δ47 standardization errors in this session.

2.2.3 Plotting Δ47 or Δ48 residuals
data47.plot_residuals(filename = 'residuals.pdf')

Again, note that this plot only shows the succession of analyses as if they were all distributed at regular time intervals.
2.3 Use a different set of anchors, change anchor nominal values, and/or change oxygen-17 correction parameters
Nominal values for various carbonate standards are defined in four places:
D4xdata.Nominal_d13C_VPDBD4xdata.Nominal_d18O_VPDBD47data.Nominal_D4x(also accessible throughD47data.Nominal_D47)D48data.Nominal_D4x(also accessible throughD48data.Nominal_D48)
17O correction parameters are defined by:
D4xdata.R13_VPDBD4xdata.R18_VSMOWD4xdata.R18_VPDBD4xdata.LAMBDA_17D4xdata.R17_VSMOWD4xdata.R17_VPDB
When creating a new instance of D47data or D48data, the current values of these variables are copied as properties of the new object. Applying custom values for, e.g., R17_VSMOW and Nominal_D47 can thus be done in several ways:
Option 1: by redefining D4xdata.R17_VSMOW and D47data.Nominal_D47 _before_ creating a D47data object:
from D47crunch import D4xdata, D47data
# redefine R17_VSMOW:
D4xdata.R17_VSMOW = 0.00037 # new value
# redefine R17_VPDB for consistency:
D4xdata.R17_VPDB = D4xdata.R17_VSMOW * (D4xdata.R18_VPDB/D4xdata.R18_VSMOW) ** D4xdata.LAMBDA_17
# edit Nominal_D47 to only include ETH-1/2/3:
D47data.Nominal_D4x = {
a: D47data.Nominal_D4x[a]
for a in ['ETH-1', 'ETH-2', 'ETH-3']
}
# redefine ETH-3:
D47data.Nominal_D4x['ETH-3'] = 0.600
# only now create D47data object:
mydata = D47data()
# check the results:
print(mydata.R17_VSMOW, mydata.R17_VPDB)
print(mydata.Nominal_D47)
# NB: mydata.Nominal_D47 is just an alias for mydata.Nominal_D4x
# should print out:
# 0.00037 0.00037599710894149464
# {'ETH-1': 0.2052, 'ETH-2': 0.2085, 'ETH-3': 0.6}
Option 2: by redefining R17_VSMOW and Nominal_D47 _after_ creating a D47data object:
from D47crunch import D47data
# first create D47data object:
mydata = D47data()
# redefine R17_VSMOW:
mydata.R17_VSMOW = 0.00037 # new value
# redefine R17_VPDB for consistency:
mydata.R17_VPDB = mydata.R17_VSMOW * (mydata.R18_VPDB/mydata.R18_VSMOW) ** mydata.LAMBDA_17
# edit Nominal_D47 to only include ETH-1/2/3:
mydata.Nominal_D47 = {
a: mydata.Nominal_D47[a]
for a in ['ETH-1', 'ETH-2', 'ETH-3']
}
# redefine ETH-3:
mydata.Nominal_D47['ETH-3'] = 0.600
# check the results:
print(mydata.R17_VSMOW, mydata.R17_VPDB)
print(mydata.Nominal_D47)
# should print out:
# 0.00037 0.00037599710894149464
# {'ETH-1': 0.2052, 'ETH-2': 0.2085, 'ETH-3': 0.6}
The two options above are equivalent, but the latter provides a simple way to compare different data processing choices:
from D47crunch import D47data
# create two D47data objects:
foo = D47data()
bar = D47data()
# modify foo in various ways:
foo.LAMBDA_17 = 0.52
foo.R17_VSMOW = 0.00037 # new value
foo.R17_VPDB = foo.R17_VSMOW * (foo.R18_VPDB/foo.R18_VSMOW) ** foo.LAMBDA_17
foo.Nominal_D47 = {
'ETH-1': foo.Nominal_D47['ETH-1'],
'ETH-2': foo.Nominal_D47['ETH-1'],
'IAEA-C2': foo.Nominal_D47['IAEA-C2'],
'INLAB_REF_MATERIAL': 0.666,
}
# now import the same raw data into foo and bar:
foo.read('rawdata.csv')
foo.wg() # compute δ13C, δ18O of working gas
foo.crunch() # compute all δ13C, δ18O and raw Δ47 values
foo.standardize() # compute absolute Δ47 values
bar.read('rawdata.csv')
bar.wg() # compute δ13C, δ18O of working gas
bar.crunch() # compute all δ13C, δ18O and raw Δ47 values
bar.standardize() # compute absolute Δ47 values
# and compare the final results:
foo.table_of_samples(verbose = True, save_to_file = False)
bar.table_of_samples(verbose = True, save_to_file = False)
2.4 Process paired Δ47 and Δ48 values
Purely in terms of data processing, it is not obvious why Δ47 and Δ48 data should not be handled separately. For now, D47crunch uses two independent classes — D47data and D48data — which crunch numbers and deal with standardization in very similar ways. The following example demonstrates how to print out combined outputs for D47data and D48data.
from D47crunch import *
# generate virtual data:
args = dict(
samples = [
dict(Sample = 'ETH-1', N = 3),
dict(Sample = 'ETH-2', N = 3),
dict(Sample = 'ETH-3', N = 3),
dict(Sample = 'FOO', N = 3,
d13C_VPDB = -5., d18O_VPDB = -10.,
D47 = 0.3, D48 = 0.15),
], rD47 = 0.010, rD48 = 0.030)
session1 = virtual_data(session = 'Session_01', **args)
session2 = virtual_data(session = 'Session_02', **args)
# create D47data instance:
data47 = D47data(session1 + session2)
# process D47data instance:
data47.crunch()
data47.standardize()
# create D48data instance:
data48 = D48data(data47) # alternatively: data48 = D48data(session1 + session2)
# process D48data instance:
data48.crunch()
data48.standardize()
# output combined results:
table_of_sessions(data47, data48)
table_of_samples(data47, data48)
table_of_analyses(data47, data48)
Expected output:
–––––––––– –– –– ––––––––––– –––––––––––– –––––– –––––– –––––– ––––––––––––– ––––––––––––––– –––––––––––––– –––––– ––––––––––––– ––––––––––––––– ––––––––––––––
Session Na Nu d13Cwg_VPDB d18Owg_VSMOW r_d13C r_d18O r_D47 a_47 ± SE 1e3 x b_47 ± SE c_47 ± SE r_D48 a_48 ± SE 1e3 x b_48 ± SE c_48 ± SE
–––––––––– –– –– ––––––––––– –––––––––––– –––––– –––––– –––––– ––––––––––––– ––––––––––––––– –––––––––––––– –––––– ––––––––––––– ––––––––––––––– ––––––––––––––
Session_01 9 3 -4.000 26.000 0.0000 0.0000 0.0098 1.021 ± 0.019 -0.398 ± 0.260 -0.903 ± 0.006 0.0486 0.540 ± 0.151 1.235 ± 0.607 -0.390 ± 0.025
Session_02 9 3 -4.000 26.000 0.0000 0.0000 0.0090 1.015 ± 0.019 0.376 ± 0.260 -0.905 ± 0.006 0.0186 1.350 ± 0.156 -0.871 ± 0.608 -0.504 ± 0.027
–––––––––– –– –– ––––––––––– –––––––––––– –––––– –––––– –––––– ––––––––––––– ––––––––––––––– –––––––––––––– –––––– ––––––––––––– ––––––––––––––– ––––––––––––––
–––––– – ––––––––– –––––––––– –––––– –––––– –––––––– –––––– –––––––– –––––– –––––– –––––––– –––––– ––––––––
Sample N d13C_VPDB d18O_VSMOW D47 SE 95% CL SD p_Levene D48 SE 95% CL SD p_Levene
–––––– – ––––––––– –––––––––– –––––– –––––– –––––––– –––––– –––––––– –––––– –––––– –––––––– –––––– ––––––––
ETH-1 6 2.02 37.02 0.2052 0.0078 0.1380 0.0223
ETH-2 6 -10.17 19.88 0.2085 0.0036 0.1380 0.0482
ETH-3 6 1.71 37.45 0.6132 0.0080 0.2700 0.0176
FOO 6 -5.00 28.91 0.3026 0.0044 ± 0.0093 0.0121 0.164 0.1397 0.0121 ± 0.0255 0.0267 0.127
–––––– – ––––––––– –––––––––– –––––– –––––– –––––––– –––––– –––––––– –––––– –––––– –––––––– –––––– ––––––––
––– –––––––––– –––––– ––––––––––– –––––––––––– ––––––––– ––––––––– –––––––––– –––––––––– –––––––––– –––––––––– –––––––––– ––––––––– ––––––––– ––––––––– –––––––– ––––––––
UID Session Sample d13Cwg_VPDB d18Owg_VSMOW d45 d46 d47 d48 d49 d13C_VPDB d18O_VSMOW D47raw D48raw D49raw D47 D48
––– –––––––––– –––––– ––––––––––– –––––––––––– ––––––––– ––––––––– –––––––––– –––––––––– –––––––––– –––––––––– –––––––––– ––––––––– ––––––––– ––––––––– –––––––– ––––––––
1 Session_01 ETH-1 -4.000 26.000 6.018962 10.747026 16.120787 21.286237 27.780042 2.020000 37.024281 -0.708176 -0.316435 -0.000013 0.197297 0.087763
2 Session_01 ETH-1 -4.000 26.000 6.018962 10.747026 16.132240 21.307795 27.780042 2.020000 37.024281 -0.696913 -0.295333 -0.000013 0.208328 0.126791
3 Session_01 ETH-1 -4.000 26.000 6.018962 10.747026 16.132438 21.313884 27.780042 2.020000 37.024281 -0.696718 -0.289374 -0.000013 0.208519 0.137813
4 Session_01 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.700300 -12.210735 -18.023381 -10.170000 19.875825 -0.683938 -0.297902 -0.000002 0.209785 0.198705
5 Session_01 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.707421 -12.270781 -18.023381 -10.170000 19.875825 -0.691145 -0.358673 -0.000002 0.202726 0.086308
6 Session_01 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.700061 -12.278310 -18.023381 -10.170000 19.875825 -0.683696 -0.366292 -0.000002 0.210022 0.072215
7 Session_01 ETH-3 -4.000 26.000 5.742374 11.161270 16.684379 22.225827 28.306614 1.710000 37.450394 -0.273094 -0.216392 -0.000014 0.623472 0.270873
8 Session_01 ETH-3 -4.000 26.000 5.742374 11.161270 16.660163 22.233729 28.306614 1.710000 37.450394 -0.296906 -0.208664 -0.000014 0.600150 0.285167
9 Session_01 ETH-3 -4.000 26.000 5.742374 11.161270 16.675191 22.215632 28.306614 1.710000 37.450394 -0.282128 -0.226363 -0.000014 0.614623 0.252432
10 Session_01 FOO -4.000 26.000 -0.840413 2.828738 1.328380 5.374933 4.665655 -5.000000 28.907344 -0.582131 -0.288924 -0.000006 0.314928 0.175105
11 Session_01 FOO -4.000 26.000 -0.840413 2.828738 1.302220 5.384454 4.665655 -5.000000 28.907344 -0.608241 -0.279457 -0.000006 0.289356 0.192614
12 Session_01 FOO -4.000 26.000 -0.840413 2.828738 1.322530 5.372841 4.665655 -5.000000 28.907344 -0.587970 -0.291004 -0.000006 0.309209 0.171257
13 Session_02 ETH-1 -4.000 26.000 6.018962 10.747026 16.140853 21.267202 27.780042 2.020000 37.024281 -0.688442 -0.335067 -0.000013 0.207730 0.138730
14 Session_02 ETH-1 -4.000 26.000 6.018962 10.747026 16.127087 21.256983 27.780042 2.020000 37.024281 -0.701980 -0.345071 -0.000013 0.194396 0.131311
15 Session_02 ETH-1 -4.000 26.000 6.018962 10.747026 16.148253 21.287779 27.780042 2.020000 37.024281 -0.681165 -0.314926 -0.000013 0.214898 0.153668
16 Session_02 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.715859 -12.204791 -18.023381 -10.170000 19.875825 -0.699685 -0.291887 -0.000002 0.207349 0.149128
17 Session_02 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.709763 -12.188685 -18.023381 -10.170000 19.875825 -0.693516 -0.275587 -0.000002 0.213426 0.161217
18 Session_02 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.715427 -12.253049 -18.023381 -10.170000 19.875825 -0.699249 -0.340727 -0.000002 0.207780 0.112907
19 Session_02 ETH-3 -4.000 26.000 5.742374 11.161270 16.685994 22.249463 28.306614 1.710000 37.450394 -0.271506 -0.193275 -0.000014 0.618328 0.244431
20 Session_02 ETH-3 -4.000 26.000 5.742374 11.161270 16.681351 22.298166 28.306614 1.710000 37.450394 -0.276071 -0.145641 -0.000014 0.613831 0.279758
21 Session_02 ETH-3 -4.000 26.000 5.742374 11.161270 16.676169 22.306848 28.306614 1.710000 37.450394 -0.281167 -0.137150 -0.000014 0.608813 0.286056
22 Session_02 FOO -4.000 26.000 -0.840413 2.828738 1.324359 5.339497 4.665655 -5.000000 28.907344 -0.586144 -0.324160 -0.000006 0.314015 0.136535
23 Session_02 FOO -4.000 26.000 -0.840413 2.828738 1.297658 5.325854 4.665655 -5.000000 28.907344 -0.612794 -0.337727 -0.000006 0.287767 0.126473
24 Session_02 FOO -4.000 26.000 -0.840413 2.828738 1.310185 5.339898 4.665655 -5.000000 28.907344 -0.600291 -0.323761 -0.000006 0.300082 0.136830
––– –––––––––– –––––– ––––––––––– –––––––––––– ––––––––– ––––––––– –––––––––– –––––––––– –––––––––– –––––––––– –––––––––– ––––––––– ––––––––– ––––––––– –––––––– ––––––––
<<<<<<< HEAD
=======
API Documentation
>>>>>>> master1''' 2Standardization and analytical error propagation of Δ47 and Δ48 clumped-isotope measurements 3 4Process and standardize carbonate and/or CO2 clumped-isotope analyses, 5from low-level data out of a dual-inlet mass spectrometer to final, “absolute” 6Δ47 and Δ48 values with fully propagated analytical error estimates 7([Daëron, 2021](https://doi.org/10.1029/2020GC009592)). 8 9The **tutorial** section takes you through a series of simple steps to import/process data and print out the results. 10The **how-to** section provides instructions applicable to various specific tasks. 11 12.. include:: ../docs/tutorial.md 13.. include:: ../docs/howto.md <<<<<<< HEAD 14''' 15 16__docformat__ = "restructuredtext" 17__author__ = 'Mathieu Daëron' 18__contact__ = 'daeron@lsce.ipsl.fr' 19__copyright__ = 'Copyright (c) 2023 Mathieu Daëron' 20__license__ = 'Modified BSD License - https://opensource.org/licenses/BSD-3-Clause' 21__date__ = '2023-05-11' 22__version__ = '2.0.4' 23 24import os 25import numpy as np 26from statistics import stdev 27from scipy.stats import t as tstudent 28from scipy.stats import levene 29from scipy.interpolate import interp1d 30from numpy import linalg 31from lmfit import Minimizer, Parameters, report_fit 32from matplotlib import pyplot as ppl 33from datetime import datetime as dt 34from functools import wraps 35from colorsys import hls_to_rgb 36from matplotlib import rcParams 37 38rcParams['font.family'] = 'sans-serif' 39rcParams['font.sans-serif'] = 'Helvetica' 40rcParams['font.size'] = 10 41rcParams['mathtext.fontset'] = 'custom' 42rcParams['mathtext.rm'] = 'sans' 43rcParams['mathtext.bf'] = 'sans:bold' 44rcParams['mathtext.it'] = 'sans:italic' 45rcParams['mathtext.cal'] = 'sans:italic' 46rcParams['mathtext.default'] = 'rm' 47rcParams['xtick.major.size'] = 4 48rcParams['xtick.major.width'] = 1 49rcParams['ytick.major.size'] = 4 50rcParams['ytick.major.width'] = 1 51rcParams['axes.grid'] = False 52rcParams['axes.linewidth'] = 1 53rcParams['grid.linewidth'] = .75 54rcParams['grid.linestyle'] = '-' 55rcParams['grid.alpha'] = .15 56rcParams['savefig.dpi'] = 150 57 58Petersen_etal_CO2eqD47 = np.array([[-12, 1.147113572], [-11, 1.139961218], [-10, 1.132872856], [-9, 1.125847677], [-8, 1.118884889], [-7, 1.111983708], [-6, 1.105143366], [-5, 1.098363105], [-4, 1.091642182], [-3, 1.084979862], [-2, 1.078375423], [-1, 1.071828156], [0, 1.065337360], [1, 1.058902349], [2, 1.052522443], [3, 1.046196976], [4, 1.039925291], [5, 1.033706741], [6, 1.027540690], [7, 1.021426510], [8, 1.015363585], [9, 1.009351306], [10, 1.003389075], [11, 0.997476303], [12, 0.991612409], [13, 0.985796821], [14, 0.980028975], [15, 0.974308318], [16, 0.968634304], [17, 0.963006392], [18, 0.957424055], [19, 0.951886769], [20, 0.946394020], [21, 0.940945302], [22, 0.935540114], [23, 0.930177964], [24, 0.924858369], [25, 0.919580851], [26, 0.914344938], [27, 0.909150167], [28, 0.903996080], [29, 0.898882228], [30, 0.893808167], [31, 0.888773459], [32, 0.883777672], [33, 0.878820382], [34, 0.873901170], [35, 0.869019623], [36, 0.864175334], [37, 0.859367901], [38, 0.854596929], [39, 0.849862028], [40, 0.845162813], [41, 0.840498905], [42, 0.835869931], [43, 0.831275522], [44, 0.826715314], [45, 0.822188950], [46, 0.817696075], [47, 0.813236341], [48, 0.808809404], [49, 0.804414926], [50, 0.800052572], [51, 0.795722012], [52, 0.791422922], [53, 0.787154979], [54, 0.782917869], [55, 0.778711277], [56, 0.774534898], [57, 0.770388426], [58, 0.766271562], [59, 0.762184010], [60, 0.758125479], [61, 0.754095680], [62, 0.750094329], [63, 0.746121147], [64, 0.742175856], [65, 0.738258184], [66, 0.734367860], [67, 0.730504620], [68, 0.726668201], [69, 0.722858343], [70, 0.719074792], [71, 0.715317295], [72, 0.711585602], [73, 0.707879469], [74, 0.704198652], [75, 0.700542912], [76, 0.696912012], [77, 0.693305719], [78, 0.689723802], [79, 0.686166034], [80, 0.682632189], [81, 0.679122047], [82, 0.675635387], [83, 0.672171994], [84, 0.668731654], [85, 0.665314156], [86, 0.661919291], [87, 0.658546854], [88, 0.655196641], [89, 0.651868451], [90, 0.648562087], [91, 0.645277352], [92, 0.642014054], [93, 0.638771999], [94, 0.635551001], [95, 0.632350872], [96, 0.629171428], [97, 0.626012487], [98, 0.622873870], [99, 0.619755397], [100, 0.616656895], [102, 0.610519107], [104, 0.604459143], [106, 0.598475670], [108, 0.592567388], [110, 0.586733026], [112, 0.580971342], [114, 0.575281125], [116, 0.569661187], [118, 0.564110371], [120, 0.558627545], [122, 0.553211600], [124, 0.547861454], [126, 0.542576048], [128, 0.537354347], [130, 0.532195337], [132, 0.527098028], [134, 0.522061450], [136, 0.517084654], [138, 0.512166711], [140, 0.507306712], [142, 0.502503768], [144, 0.497757006], [146, 0.493065573], [148, 0.488428634], [150, 0.483845370], [152, 0.479314980], [154, 0.474836677], [156, 0.470409692], [158, 0.466033271], [160, 0.461706674], [162, 0.457429176], [164, 0.453200067], [166, 0.449018650], [168, 0.444884242], [170, 0.440796174], [172, 0.436753787], [174, 0.432756438], [176, 0.428803494], [178, 0.424894334], [180, 0.421028350], [182, 0.417204944], [184, 0.413423530], [186, 0.409683531], [188, 0.405984383], [190, 0.402325531], [192, 0.398706429], [194, 0.395126543], [196, 0.391585347], [198, 0.388082324], [200, 0.384616967], [202, 0.381188778], [204, 0.377797268], [206, 0.374441954], [208, 0.371122364], [210, 0.367838033], [212, 0.364588505], [214, 0.361373329], [216, 0.358192065], [218, 0.355044277], [220, 0.351929540], [222, 0.348847432], [224, 0.345797540], [226, 0.342779460], [228, 0.339792789], [230, 0.336837136], [232, 0.333912113], [234, 0.331017339], [236, 0.328152439], [238, 0.325317046], [240, 0.322510795], [242, 0.319733329], [244, 0.316984297], [246, 0.314263352], [248, 0.311570153], [250, 0.308904364], [252, 0.306265654], [254, 0.303653699], [256, 0.301068176], [258, 0.298508771], [260, 0.295975171], [262, 0.293467070], [264, 0.290984167], [266, 0.288526163], [268, 0.286092765], [270, 0.283683684], [272, 0.281298636], [274, 0.278937339], [276, 0.276599517], [278, 0.274284898], [280, 0.271993211], [282, 0.269724193], [284, 0.267477582], [286, 0.265253121], [288, 0.263050554], [290, 0.260869633], [292, 0.258710110], [294, 0.256571741], [296, 0.254454286], [298, 0.252357508], [300, 0.250281174], [302, 0.248225053], [304, 0.246188917], [306, 0.244172542], [308, 0.242175707], [310, 0.240198194], [312, 0.238239786], [314, 0.236300272], [316, 0.234379441], [318, 0.232477087], [320, 0.230593005], [322, 0.228726993], [324, 0.226878853], [326, 0.225048388], [328, 0.223235405], [330, 0.221439711], [332, 0.219661118], [334, 0.217899439], [336, 0.216154491], [338, 0.214426091], [340, 0.212714060], [342, 0.211018220], [344, 0.209338398], [346, 0.207674420], [348, 0.206026115], [350, 0.204393315], [355, 0.200378063], [360, 0.196456139], [365, 0.192625077], [370, 0.188882487], [375, 0.185226048], [380, 0.181653511], [385, 0.178162694], [390, 0.174751478], [395, 0.171417807], [400, 0.168159686], [405, 0.164975177], [410, 0.161862398], [415, 0.158819521], [420, 0.155844772], [425, 0.152936426], [430, 0.150092806], [435, 0.147312286], [440, 0.144593281], [445, 0.141934254], [450, 0.139333710], [455, 0.136790195], [460, 0.134302294], [465, 0.131868634], [470, 0.129487876], [475, 0.127158722], [480, 0.124879906], [485, 0.122650197], [490, 0.120468398], [495, 0.118333345], [500, 0.116243903], [505, 0.114198970], [510, 0.112197471], [515, 0.110238362], [520, 0.108320625], [525, 0.106443271], [530, 0.104605335], [535, 0.102805877], [540, 0.101043985], [545, 0.099318768], [550, 0.097629359], [555, 0.095974915], [560, 0.094354612], [565, 0.092767650], [570, 0.091213248], [575, 0.089690648], [580, 0.088199108], [585, 0.086737906], [590, 0.085306341], [595, 0.083903726], [600, 0.082529395], [605, 0.081182697], [610, 0.079862998], [615, 0.078569680], [620, 0.077302141], [625, 0.076059794], [630, 0.074842066], [635, 0.073648400], [640, 0.072478251], [645, 0.071331090], [650, 0.070206399], [655, 0.069103674], [660, 0.068022424], [665, 0.066962168], [670, 0.065922439], [675, 0.064902780], [680, 0.063902748], [685, 0.062921909], [690, 0.061959837], [695, 0.061016122], [700, 0.060090360], [705, 0.059182157], [710, 0.058291131], [715, 0.057416907], [720, 0.056559120], [725, 0.055717414], [730, 0.054891440], [735, 0.054080860], [740, 0.053285343], [745, 0.052504565], [750, 0.051738210], [755, 0.050985971], [760, 0.050247546], [765, 0.049522643], [770, 0.048810974], [775, 0.048112260], [780, 0.047426227], [785, 0.046752609], [790, 0.046091145], [795, 0.045441581], [800, 0.044803668], [805, 0.044177164], [810, 0.043561831], [815, 0.042957438], [820, 0.042363759], [825, 0.041780573], [830, 0.041207664], [835, 0.040644822], [840, 0.040091839], [845, 0.039548516], [850, 0.039014654], [855, 0.038490063], [860, 0.037974554], [865, 0.037467944], [870, 0.036970054], [875, 0.036480707], [880, 0.035999734], [885, 0.035526965], [890, 0.035062238], [895, 0.034605393], [900, 0.034156272], [905, 0.033714724], [910, 0.033280598], [915, 0.032853749], [920, 0.032434032], [925, 0.032021309], [930, 0.031615443], [935, 0.031216300], [940, 0.030823749], [945, 0.030437663], [950, 0.030057915], [955, 0.029684385], [960, 0.029316951], [965, 0.028955498], [970, 0.028599910], [975, 0.028250075], [980, 0.027905884], [985, 0.027567229], [990, 0.027234006], [995, 0.026906112], [1000, 0.026583445], [1005, 0.026265908], [1010, 0.025953405], [1015, 0.025645841], [1020, 0.025343124], [1025, 0.025045163], [1030, 0.024751871], [1035, 0.024463160], [1040, 0.024178947], [1045, 0.023899147], [1050, 0.023623680], [1055, 0.023352467], [1060, 0.023085429], [1065, 0.022822491], [1070, 0.022563577], [1075, 0.022308615], [1080, 0.022057533], [1085, 0.021810260], [1090, 0.021566729], [1095, 0.021326872], [1100, 0.021090622]]) 59_fCO2eqD47_Petersen = interp1d(Petersen_etal_CO2eqD47[:,0], Petersen_etal_CO2eqD47[:,1]) 60def fCO2eqD47_Petersen(T): 61 ''' 62 CO2 equilibrium Δ47 value as a function of T (in degrees C) 63 according to [Petersen et al. (2019)](https://doi.org/10.1029/2018GC008127). 64 65 ''' 66 return float(_fCO2eqD47_Petersen(T)) 67 68 69Wang_etal_CO2eqD47 = np.array([[-83., 1.8954], [-73., 1.7530], [-63., 1.6261], [-53., 1.5126], [-43., 1.4104], [-33., 1.3182], [-23., 1.2345], [-13., 1.1584], [-3., 1.0888], [7., 1.0251], [17., 0.9665], [27., 0.9125], [37., 0.8626], [47., 0.8164], [57., 0.7734], [67., 0.7334], [87., 0.6612], [97., 0.6286], [107., 0.5980], [117., 0.5693], [127., 0.5423], [137., 0.5169], [147., 0.4930], [157., 0.4704], [167., 0.4491], [177., 0.4289], [187., 0.4098], [197., 0.3918], [207., 0.3747], [217., 0.3585], [227., 0.3431], [237., 0.3285], [247., 0.3147], [257., 0.3015], [267., 0.2890], [277., 0.2771], [287., 0.2657], [297., 0.2550], [307., 0.2447], [317., 0.2349], [327., 0.2256], [337., 0.2167], [347., 0.2083], [357., 0.2002], [367., 0.1925], [377., 0.1851], [387., 0.1781], [397., 0.1714], [407., 0.1650], [417., 0.1589], [427., 0.1530], [437., 0.1474], [447., 0.1421], [457., 0.1370], [467., 0.1321], [477., 0.1274], [487., 0.1229], [497., 0.1186], [507., 0.1145], [517., 0.1105], [527., 0.1068], [537., 0.1031], [547., 0.0997], [557., 0.0963], [567., 0.0931], [577., 0.0901], [587., 0.0871], [597., 0.0843], [607., 0.0816], [617., 0.0790], [627., 0.0765], [637., 0.0741], [647., 0.0718], [657., 0.0695], [667., 0.0674], [677., 0.0654], [687., 0.0634], [697., 0.0615], [707., 0.0597], [717., 0.0579], [727., 0.0562], [737., 0.0546], [747., 0.0530], [757., 0.0515], [767., 0.0500], [777., 0.0486], [787., 0.0472], [797., 0.0459], [807., 0.0447], [817., 0.0435], [827., 0.0423], [837., 0.0411], [847., 0.0400], [857., 0.0390], [867., 0.0380], [877., 0.0370], [887., 0.0360], [897., 0.0351], [907., 0.0342], [917., 0.0333], [927., 0.0325], [937., 0.0317], [947., 0.0309], [957., 0.0302], [967., 0.0294], [977., 0.0287], [987., 0.0281], [997., 0.0274], [1007., 0.0268], [1017., 0.0261], [1027., 0.0255], [1037., 0.0249], [1047., 0.0244], [1057., 0.0238], [1067., 0.0233], [1077., 0.0228], [1087., 0.0223], [1097., 0.0218]]) 70_fCO2eqD47_Wang = interp1d(Wang_etal_CO2eqD47[:,0] - 0.15, Wang_etal_CO2eqD47[:,1]) 71def fCO2eqD47_Wang(T): 72 ''' 73 CO2 equilibrium Δ47 value as a function of `T` (in degrees C) 74 according to [Wang et al. (2004)](https://doi.org/10.1016/j.gca.2004.05.039) 75 (supplementary data of [Dennis et al., 2011](https://doi.org/10.1016/j.gca.2011.09.025)). 76 ''' 77 return float(_fCO2eqD47_Wang(T)) 78 79 80def correlated_sum(X, C, w = None): 81 ''' 82 Compute covariance-aware linear combinations 83 84 **Parameters** 85 86 + `X`: list or 1-D array of values to sum 87 + `C`: covariance matrix for the elements of `X` 88 + `w`: list or 1-D array of weights to apply to the elements of `X` 89 (all equal to 1 by default) 90 91 Return the sum (and its SE) of the elements of `X`, with optional weights equal 92 to the elements of `w`, accounting for covariances between the elements of `X`. 93 ''' 94 if w is None: 95 w = [1 for x in X] 96 return np.dot(w,X), (np.dot(w,np.dot(C,w)))**.5 97 98 99def make_csv(x, hsep = ',', vsep = '\n'): 100 ''' 101 Formats a list of lists of strings as a CSV 102 103 **Parameters** 104 105 + `x`: the list of lists of strings to format 106 + `hsep`: the field separator (`,` by default) 107 + `vsep`: the line-ending convention to use (`\\n` by default) 108 109 **Example** 110 111 ```py 112 print(make_csv([['a', 'b', 'c'], ['d', 'e', 'f']])) 113 ``` 114 115 outputs: 116 117 ```py 118 a,b,c 119 d,e,f 120 ``` 121 ''' 122 return vsep.join([hsep.join(l) for l in x]) 123 124 125def pf(txt): 126 ''' 127 Modify string `txt` to follow `lmfit.Parameter()` naming rules. 128 ''' 129 return txt.replace('-','_').replace('.','_').replace(' ','_') 130 131 132def smart_type(x): 133 ''' 134 Tries to convert string `x` to a float if it includes a decimal point, or 135 to an integer if it does not. If both attempts fail, return the original 136 string unchanged. 137 ''' 138 try: 139 y = float(x) 140 except ValueError: 141 return x 142 if '.' not in x: 143 return int(y) 144 return y 145 146 147def pretty_table(x, header = 1, hsep = ' ', vsep = '–', align = '<'): 148 ''' 149 Reads a list of lists of strings and outputs an ascii table 150 151 **Parameters** 152 153 + `x`: a list of lists of strings 154 + `header`: the number of lines to treat as header lines 155 + `hsep`: the horizontal separator between columns 156 + `vsep`: the character to use as vertical separator 157 + `align`: string of left (`<`) or right (`>`) alignment characters. 158 159 **Example** 160 161 ```py 162 x = [['A', 'B', 'C'], ['1', '1.9999', 'foo'], ['10', 'x', 'bar']] 163 print(pretty_table(x)) 164 ``` 165 yields: 166 ``` 167 -- ------ --- 168 A B C 169 -- ------ --- 170 1 1.9999 foo 171 10 x bar 172 -- ------ --- 173 ``` 174 175 ''' 176 txt = [] 177 widths = [np.max([len(e) for e in c]) for c in zip(*x)] 178 179 if len(widths) > len(align): 180 align += '>' * (len(widths)-len(align)) 181 sepline = hsep.join([vsep*w for w in widths]) 182 txt += [sepline] 183 for k,l in enumerate(x): 184 if k and k == header: 185 txt += [sepline] 186 txt += [hsep.join([f'{e:{a}{w}}' for e, w, a in zip(l, widths, align)])] 187 txt += [sepline] 188 txt += [''] 189 return '\n'.join(txt) 190 191 192def transpose_table(x): 193 ''' 194 Transpose a list if lists 195 196 **Parameters** 197 198 + `x`: a list of lists 199 200 **Example** 201 202 ```py 203 x = [[1, 2], [3, 4]] 204 print(transpose_table(x)) # yields: [[1, 3], [2, 4]] 205 ``` 206 ''' 207 return [[e for e in c] for c in zip(*x)] 208 209 210def w_avg(X, sX) : 211 ''' 212 Compute variance-weighted average 213 214 Returns the value and SE of the weighted average of the elements of `X`, 215 with relative weights equal to their inverse variances (`1/sX**2`). 216 217 **Parameters** 218 219 + `X`: array-like of elements to average 220 + `sX`: array-like of the corresponding SE values 221 222 **Tip** 223 224 If `X` and `sX` are initially arranged as a list of `(x, sx)` doublets, 225 they may be rearranged using `zip()`: 226 227 ```python 228 foo = [(0, 1), (1, 0.5), (2, 0.5)] 229 print(w_avg(*zip(*foo))) # yields: (1.3333333333333333, 0.3333333333333333) 230 ``` 231 ''' 232 X = [ x for x in X ] 233 sX = [ sx for sx in sX ] 234 W = [ sx**-2 for sx in sX ] 235 W = [ w/sum(W) for w in W ] 236 Xavg = sum([ w*x for w,x in zip(W,X) ]) 237 sXavg = sum([ w**2*sx**2 for w,sx in zip(W,sX) ])**.5 238 return Xavg, sXavg 239 240 241def read_csv(filename, sep = ''): 242 ''' 243 Read contents of `filename` in csv format and return a list of dictionaries. 244 245 In the csv string, spaces before and after field separators (`','` by default) 246 are optional. 247 248 **Parameters** 249 250 + `filename`: the csv file to read 251 + `sep`: csv separator delimiting the fields. By default, use `,`, `;`, or `\t`, 252 whichever appers most often in the contents of `filename`. 253 ''' 254 with open(filename) as fid: 255 txt = fid.read() 256 257 if sep == '': 258 sep = sorted(',;\t', key = lambda x: - txt.count(x))[0] 259 txt = [[x.strip() for x in l.split(sep)] for l in txt.splitlines() if l.strip()] 260 return [{k: smart_type(v) for k,v in zip(txt[0], l) if v} for l in txt[1:]] 261 262 263def simulate_single_analysis( 264 sample = 'MYSAMPLE', 265 d13Cwg_VPDB = -4., d18Owg_VSMOW = 26., 266 d13C_VPDB = None, d18O_VPDB = None, 267 D47 = None, D48 = None, D49 = 0., D17O = 0., 268 a47 = 1., b47 = 0., c47 = -0.9, 269 a48 = 1., b48 = 0., c48 = -0.45, 270 Nominal_D47 = None, 271 Nominal_D48 = None, 272 Nominal_d13C_VPDB = None, 273 Nominal_d18O_VPDB = None, 274 ALPHA_18O_ACID_REACTION = None, 275 R13_VPDB = None, 276 R17_VSMOW = None, 277 R18_VSMOW = None, 278 LAMBDA_17 = None, 279 R18_VPDB = None, 280 ): 281 ''' 282 Compute working-gas delta values for a single analysis, assuming a stochastic working 283 gas and a “perfect” measurement (i.e. raw Δ values are identical to absolute values). 284 285 **Parameters** 286 287 + `sample`: sample name 288 + `d13Cwg_VPDB`, `d18Owg_VSMOW`: bulk composition of the working gas 289 (respectively –4 and +26 ‰ by default) 290 + `d13C_VPDB`, `d18O_VPDB`: bulk composition of the carbonate sample 291 + `D47`, `D48`, `D49`, `D17O`: clumped-isotope and oxygen-17 anomalies 292 of the carbonate sample 293 + `Nominal_D47`, `Nominal_D48`: where to lookup Δ47 and 294 Δ48 values if `D47` or `D48` are not specified 295 + `Nominal_d13C_VPDB`, `Nominal_d18O_VPDB`: where to lookup δ13C and 296 δ18O values if `d13C_VPDB` or `d18O_VPDB` are not specified 297 + `ALPHA_18O_ACID_REACTION`: 18O/16O acid fractionation factor 298 + `R13_VPDB`, `R17_VSMOW`, `R18_VSMOW`, `LAMBDA_17`, `R18_VPDB`: oxygen-17 299 correction parameters (by default equal to the `D4xdata` default values) 300 301 Returns a dictionary with fields 302 `['Sample', 'D17O', 'd13Cwg_VPDB', 'd18Owg_VSMOW', 'd45', 'd46', 'd47', 'd48', 'd49']`. 303 ''' 304 305 if Nominal_d13C_VPDB is None: 306 Nominal_d13C_VPDB = D4xdata().Nominal_d13C_VPDB 307 308 if Nominal_d18O_VPDB is None: 309 Nominal_d18O_VPDB = D4xdata().Nominal_d18O_VPDB 310 311 if ALPHA_18O_ACID_REACTION is None: 312 ALPHA_18O_ACID_REACTION = D4xdata().ALPHA_18O_ACID_REACTION 313 314 if R13_VPDB is None: 315 R13_VPDB = D4xdata().R13_VPDB 316 317 if R17_VSMOW is None: 318 R17_VSMOW = D4xdata().R17_VSMOW 319 320 if R18_VSMOW is None: 321 R18_VSMOW = D4xdata().R18_VSMOW 322 323 if LAMBDA_17 is None: 324 LAMBDA_17 = D4xdata().LAMBDA_17 325 326 if R18_VPDB is None: 327 R18_VPDB = D4xdata().R18_VPDB 328 329 R17_VPDB = R17_VSMOW * (R18_VPDB / R18_VSMOW) ** LAMBDA_17 330 331 if Nominal_D47 is None: 332 Nominal_D47 = D47data().Nominal_D47 333 334 if Nominal_D48 is None: 335 Nominal_D48 = D48data().Nominal_D48 336 337 if d13C_VPDB is None: 338 if sample in Nominal_d13C_VPDB: 339 d13C_VPDB = Nominal_d13C_VPDB[sample] 340 else: 341 raise KeyError(f"Sample {sample} is missing d13C_VDP value, and it is not defined in Nominal_d13C_VDP.") 342 343 if d18O_VPDB is None: 344 if sample in Nominal_d18O_VPDB: 345 d18O_VPDB = Nominal_d18O_VPDB[sample] 346 else: 347 raise KeyError(f"Sample {sample} is missing d18O_VPDB value, and it is not defined in Nominal_d18O_VPDB.") 348 349 if D47 is None: 350 if sample in Nominal_D47: 351 D47 = Nominal_D47[sample] 352 else: 353 raise KeyError(f"Sample {sample} is missing D47 value, and it is not defined in Nominal_D47.") 354 355 if D48 is None: 356 if sample in Nominal_D48: 357 D48 = Nominal_D48[sample] 358 else: 359 raise KeyError(f"Sample {sample} is missing D48 value, and it is not defined in Nominal_D48.") 360 361 X = D4xdata() 362 X.R13_VPDB = R13_VPDB 363 X.R17_VSMOW = R17_VSMOW 364 X.R18_VSMOW = R18_VSMOW 365 X.LAMBDA_17 = LAMBDA_17 366 X.R18_VPDB = R18_VPDB 367 X.R17_VPDB = R17_VSMOW * (R18_VPDB / R18_VSMOW)**LAMBDA_17 368 369 R45wg, R46wg, R47wg, R48wg, R49wg = X.compute_isobar_ratios( 370 R13 = R13_VPDB * (1 + d13Cwg_VPDB/1000), 371 R18 = R18_VSMOW * (1 + d18Owg_VSMOW/1000), 372 ) 373 R45, R46, R47, R48, R49 = X.compute_isobar_ratios( 374 R13 = R13_VPDB * (1 + d13C_VPDB/1000), 375 R18 = R18_VPDB * (1 + d18O_VPDB/1000) * ALPHA_18O_ACID_REACTION, 376 D17O=D17O, D47=D47, D48=D48, D49=D49, 377 ) 378 R45stoch, R46stoch, R47stoch, R48stoch, R49stoch = X.compute_isobar_ratios( 379 R13 = R13_VPDB * (1 + d13C_VPDB/1000), 380 R18 = R18_VPDB * (1 + d18O_VPDB/1000) * ALPHA_18O_ACID_REACTION, 381 D17O=D17O, 382 ) 383 384 d45 = 1000 * (R45/R45wg - 1) 385 d46 = 1000 * (R46/R46wg - 1) 386 d47 = 1000 * (R47/R47wg - 1) 387 d48 = 1000 * (R48/R48wg - 1) 388 d49 = 1000 * (R49/R49wg - 1) 389 390 for k in range(3): # dumb iteration to adjust for small changes in d47 391 R47raw = (1 + (a47 * D47 + b47 * d47 + c47)/1000) * R47stoch 392 R48raw = (1 + (a48 * D48 + b48 * d48 + c48)/1000) * R48stoch 393 d47 = 1000 * (R47raw/R47wg - 1) 394 d48 = 1000 * (R48raw/R48wg - 1) 395 396 return dict( 397 Sample = sample, 398 D17O = D17O, 399 d13Cwg_VPDB = d13Cwg_VPDB, 400 d18Owg_VSMOW = d18Owg_VSMOW, 401 d45 = d45, 402 d46 = d46, 403 d47 = d47, 404 d48 = d48, 405 d49 = d49, 406 ) 407 408 409def virtual_data( 410 samples = [], 411 a47 = 1., b47 = 0., c47 = -0.9, 412 a48 = 1., b48 = 0., c48 = -0.45, 413 rD47 = 0.015, rD48 = 0.045, 414 d13Cwg_VPDB = None, d18Owg_VSMOW = None, 415 session = None, 416 Nominal_D47 = None, Nominal_D48 = None, 417 Nominal_d13C_VPDB = None, Nominal_d18O_VPDB = None, 418 ALPHA_18O_ACID_REACTION = None, 419 R13_VPDB = None, 420 R17_VSMOW = None, 421 R18_VSMOW = None, 422 LAMBDA_17 = None, 423 R18_VPDB = None, 424 seed = 0, 425 ): 426 ''' 427 Return list with simulated analyses from a single session. 428 429 **Parameters** 430 431 + `samples`: a list of entries; each entry is a dictionary with the following fields: 432 * `Sample`: the name of the sample 433 * `d13C_VPDB`, `d18O_VPDB`: bulk composition of the carbonate sample 434 * `D47`, `D48`, `D49`, `D17O` (all optional): clumped-isotope and oxygen-17 anomalies of the carbonate sample 435 * `N`: how many analyses to generate for this sample 436 + `a47`: scrambling factor for Δ47 437 + `b47`: compositional nonlinearity for Δ47 438 + `c47`: working gas offset for Δ47 439 + `a48`: scrambling factor for Δ48 440 + `b48`: compositional nonlinearity for Δ48 441 + `c48`: working gas offset for Δ48 442 + `rD47`: analytical repeatability of Δ47 443 + `rD48`: analytical repeatability of Δ48 444 + `d13Cwg_VPDB`, `d18Owg_VSMOW`: bulk composition of the working gas 445 (by default equal to the `simulate_single_analysis` default values) 446 + `session`: name of the session (no name by default) 447 + `Nominal_D47`, `Nominal_D48`: where to lookup Δ47 and Δ48 values 448 if `D47` or `D48` are not specified (by default equal to the `simulate_single_analysis` defaults) 449 + `Nominal_d13C_VPDB`, `Nominal_d18O_VPDB`: where to lookup δ13C and 450 δ18O values if `d13C_VPDB` or `d18O_VPDB` are not specified 451 (by default equal to the `simulate_single_analysis` defaults) 452 + `ALPHA_18O_ACID_REACTION`: 18O/16O acid fractionation factor 453 (by default equal to the `simulate_single_analysis` defaults) 454 + `R13_VPDB`, `R17_VSMOW`, `R18_VSMOW`, `LAMBDA_17`, `R18_VPDB`: oxygen-17 455 correction parameters (by default equal to the `simulate_single_analysis` default) 456 + `seed`: explicitly set to a non-zero value to achieve random but repeatable simulations 457 458 459 Here is an example of using this method to generate an arbitrary combination of 460 anchors and unknowns for a bunch of sessions: 461 462 ```py 463 args = dict( 464 samples = [ 465 dict(Sample = 'ETH-1', N = 4), 466 dict(Sample = 'ETH-2', N = 5), 467 dict(Sample = 'ETH-3', N = 6), 468 dict(Sample = 'FOO', N = 2, 469 d13C_VPDB = -5., d18O_VPDB = -10., 470 D47 = 0.3, D48 = 0.15), 471 ], rD47 = 0.010, rD48 = 0.030) 472 473 session1 = virtual_data(session = 'Session_01', **args, seed = 123) 474 session2 = virtual_data(session = 'Session_02', **args, seed = 1234) 475 session3 = virtual_data(session = 'Session_03', **args, seed = 12345) 476 session4 = virtual_data(session = 'Session_04', **args, seed = 123456) 477 478 D = D47data(session1 + session2 + session3 + session4) 479 480 D.crunch() 481 D.standardize() 482 483 D.table_of_sessions(verbose = True, save_to_file = False) 484 D.table_of_samples(verbose = True, save_to_file = False) 485 D.table_of_analyses(verbose = True, save_to_file = False) 486 ``` 487 488 This should output something like: 489 490 ``` 491 [table_of_sessions] 492 –––––––––– –– –– ––––––––––– –––––––––––– –––––– –––––– –––––– ––––––––––––– –––––––––––––– –––––––––––––– 493 Session Na Nu d13Cwg_VPDB d18Owg_VSMOW r_d13C r_d18O r_D47 a ± SE 1e3 x b ± SE c ± SE 494 –––––––––– –– –– ––––––––––– –––––––––––– –––––– –––––– –––––– ––––––––––––– –––––––––––––– –––––––––––––– 495 Session_01 15 2 -4.000 26.000 0.0000 0.0000 0.0110 0.997 ± 0.017 -0.097 ± 0.244 -0.896 ± 0.006 496 Session_02 15 2 -4.000 26.000 0.0000 0.0000 0.0109 1.002 ± 0.017 -0.110 ± 0.244 -0.901 ± 0.006 497 Session_03 15 2 -4.000 26.000 0.0000 0.0000 0.0107 1.010 ± 0.017 -0.037 ± 0.244 -0.904 ± 0.006 498 Session_04 15 2 -4.000 26.000 0.0000 0.0000 0.0106 1.001 ± 0.017 -0.181 ± 0.244 -0.894 ± 0.006 499 –––––––––– –– –– ––––––––––– –––––––––––– –––––– –––––– –––––– ––––––––––––– –––––––––––––– –––––––––––––– 500 501 [table_of_samples] 502 –––––– –– ––––––––– –––––––––– –––––– –––––– –––––––– –––––– –––––––– 503 Sample N d13C_VPDB d18O_VSMOW D47 SE 95% CL SD p_Levene 504 –––––– –– ––––––––– –––––––––– –––––– –––––– –––––––– –––––– –––––––– 505 ETH-1 16 2.02 37.02 0.2052 0.0079 506 ETH-2 20 -10.17 19.88 0.2085 0.0100 507 ETH-3 24 1.71 37.45 0.6132 0.0105 508 FOO 8 -5.00 28.91 0.2989 0.0040 ± 0.0080 0.0101 0.638 509 –––––– –– ––––––––– –––––––––– –––––– –––––– –––––––– –––––– –––––––– 510 511 [table_of_analyses] 512 ––– –––––––––– –––––– ––––––––––– –––––––––––– ––––––––– ––––––––– –––––––––– –––––––––– –––––––––– –––––––––– –––––––––– ––––––––– ––––––––– ––––––––– –––––––– 513 UID Session Sample d13Cwg_VPDB d18Owg_VSMOW d45 d46 d47 d48 d49 d13C_VPDB d18O_VSMOW D47raw D48raw D49raw D47 514 ––– –––––––––– –––––– ––––––––––– –––––––––––– ––––––––– ––––––––– –––––––––– –––––––––– –––––––––– –––––––––– –––––––––– ––––––––– ––––––––– ––––––––– –––––––– 515 1 Session_01 ETH-1 -4.000 26.000 6.018962 10.747026 16.122986 21.273526 27.780042 2.020000 37.024281 -0.706013 -0.328878 -0.000013 0.192554 516 2 Session_01 ETH-1 -4.000 26.000 6.018962 10.747026 16.130144 21.282615 27.780042 2.020000 37.024281 -0.698974 -0.319981 -0.000013 0.199615 517 3 Session_01 ETH-1 -4.000 26.000 6.018962 10.747026 16.149219 21.299572 27.780042 2.020000 37.024281 -0.680215 -0.303383 -0.000013 0.218429 518 4 Session_01 ETH-1 -4.000 26.000 6.018962 10.747026 16.136616 21.233128 27.780042 2.020000 37.024281 -0.692609 -0.368421 -0.000013 0.205998 519 5 Session_01 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.697171 -12.203054 -18.023381 -10.170000 19.875825 -0.680771 -0.290128 -0.000002 0.215054 520 6 Session_01 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.701124 -12.184422 -18.023381 -10.170000 19.875825 -0.684772 -0.271272 -0.000002 0.211041 521 7 Session_01 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.715105 -12.195251 -18.023381 -10.170000 19.875825 -0.698923 -0.282232 -0.000002 0.196848 522 8 Session_01 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.701529 -12.204963 -18.023381 -10.170000 19.875825 -0.685182 -0.292061 -0.000002 0.210630 523 9 Session_01 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.711420 -12.228478 -18.023381 -10.170000 19.875825 -0.695193 -0.315859 -0.000002 0.200589 524 10 Session_01 ETH-3 -4.000 26.000 5.742374 11.161270 16.666719 22.296486 28.306614 1.710000 37.450394 -0.290459 -0.147284 -0.000014 0.609363 525 11 Session_01 ETH-3 -4.000 26.000 5.742374 11.161270 16.671553 22.291060 28.306614 1.710000 37.450394 -0.285706 -0.152592 -0.000014 0.614130 526 12 Session_01 ETH-3 -4.000 26.000 5.742374 11.161270 16.652854 22.273271 28.306614 1.710000 37.450394 -0.304093 -0.169990 -0.000014 0.595689 527 13 Session_01 ETH-3 -4.000 26.000 5.742374 11.161270 16.684168 22.263156 28.306614 1.710000 37.450394 -0.273302 -0.179883 -0.000014 0.626572 528 14 Session_01 ETH-3 -4.000 26.000 5.742374 11.161270 16.662702 22.253578 28.306614 1.710000 37.450394 -0.294409 -0.189251 -0.000014 0.605401 529 15 Session_01 ETH-3 -4.000 26.000 5.742374 11.161270 16.681957 22.230907 28.306614 1.710000 37.450394 -0.275476 -0.211424 -0.000014 0.624391 530 16 Session_01 FOO -4.000 26.000 -0.840413 2.828738 1.312044 5.395798 4.665655 -5.000000 28.907344 -0.598436 -0.268176 -0.000006 0.298996 531 17 Session_01 FOO -4.000 26.000 -0.840413 2.828738 1.328123 5.307086 4.665655 -5.000000 28.907344 -0.582387 -0.356389 -0.000006 0.315092 532 18 Session_02 ETH-1 -4.000 26.000 6.018962 10.747026 16.122201 21.340606 27.780042 2.020000 37.024281 -0.706785 -0.263217 -0.000013 0.195135 533 19 Session_02 ETH-1 -4.000 26.000 6.018962 10.747026 16.134868 21.305714 27.780042 2.020000 37.024281 -0.694328 -0.297370 -0.000013 0.207564 534 20 Session_02 ETH-1 -4.000 26.000 6.018962 10.747026 16.140008 21.261931 27.780042 2.020000 37.024281 -0.689273 -0.340227 -0.000013 0.212607 535 21 Session_02 ETH-1 -4.000 26.000 6.018962 10.747026 16.135540 21.298472 27.780042 2.020000 37.024281 -0.693667 -0.304459 -0.000013 0.208224 536 22 Session_02 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.701213 -12.202602 -18.023381 -10.170000 19.875825 -0.684862 -0.289671 -0.000002 0.213842 537 23 Session_02 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.685649 -12.190405 -18.023381 -10.170000 19.875825 -0.669108 -0.277327 -0.000002 0.229559 538 24 Session_02 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.719003 -12.257955 -18.023381 -10.170000 19.875825 -0.702869 -0.345692 -0.000002 0.195876 539 25 Session_02 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.700592 -12.204641 -18.023381 -10.170000 19.875825 -0.684233 -0.291735 -0.000002 0.214469 540 26 Session_02 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.720426 -12.214561 -18.023381 -10.170000 19.875825 -0.704308 -0.301774 -0.000002 0.194439 541 27 Session_02 ETH-3 -4.000 26.000 5.742374 11.161270 16.673044 22.262090 28.306614 1.710000 37.450394 -0.284240 -0.180926 -0.000014 0.616730 542 28 Session_02 ETH-3 -4.000 26.000 5.742374 11.161270 16.666542 22.263401 28.306614 1.710000 37.450394 -0.290634 -0.179643 -0.000014 0.610350 543 29 Session_02 ETH-3 -4.000 26.000 5.742374 11.161270 16.680487 22.243486 28.306614 1.710000 37.450394 -0.276921 -0.199121 -0.000014 0.624031 544 30 Session_02 ETH-3 -4.000 26.000 5.742374 11.161270 16.663900 22.245175 28.306614 1.710000 37.450394 -0.293231 -0.197469 -0.000014 0.607759 545 31 Session_02 ETH-3 -4.000 26.000 5.742374 11.161270 16.674379 22.301309 28.306614 1.710000 37.450394 -0.282927 -0.142568 -0.000014 0.618039 546 32 Session_02 ETH-3 -4.000 26.000 5.742374 11.161270 16.660825 22.270466 28.306614 1.710000 37.450394 -0.296255 -0.172733 -0.000014 0.604742 547 33 Session_02 FOO -4.000 26.000 -0.840413 2.828738 1.294076 5.349940 4.665655 -5.000000 28.907344 -0.616369 -0.313776 -0.000006 0.283707 548 34 Session_02 FOO -4.000 26.000 -0.840413 2.828738 1.313775 5.292121 4.665655 -5.000000 28.907344 -0.596708 -0.371269 -0.000006 0.303323 549 35 Session_03 ETH-1 -4.000 26.000 6.018962 10.747026 16.121613 21.259909 27.780042 2.020000 37.024281 -0.707364 -0.342207 -0.000013 0.194934 550 36 Session_03 ETH-1 -4.000 26.000 6.018962 10.747026 16.145714 21.304889 27.780042 2.020000 37.024281 -0.683661 -0.298178 -0.000013 0.218401 551 37 Session_03 ETH-1 -4.000 26.000 6.018962 10.747026 16.126573 21.325093 27.780042 2.020000 37.024281 -0.702485 -0.278401 -0.000013 0.199764 552 38 Session_03 ETH-1 -4.000 26.000 6.018962 10.747026 16.132057 21.323211 27.780042 2.020000 37.024281 -0.697092 -0.280244 -0.000013 0.205104 553 39 Session_03 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.708448 -12.232023 -18.023381 -10.170000 19.875825 -0.692185 -0.319447 -0.000002 0.208915 554 40 Session_03 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.714417 -12.202504 -18.023381 -10.170000 19.875825 -0.698226 -0.289572 -0.000002 0.202934 555 41 Session_03 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.720039 -12.264469 -18.023381 -10.170000 19.875825 -0.703917 -0.352285 -0.000002 0.197300 556 42 Session_03 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.701953 -12.228550 -18.023381 -10.170000 19.875825 -0.685611 -0.315932 -0.000002 0.215423 557 43 Session_03 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.704535 -12.213634 -18.023381 -10.170000 19.875825 -0.688224 -0.300836 -0.000002 0.212837 558 44 Session_03 ETH-3 -4.000 26.000 5.742374 11.161270 16.652920 22.230043 28.306614 1.710000 37.450394 -0.304028 -0.212269 -0.000014 0.594265 559 45 Session_03 ETH-3 -4.000 26.000 5.742374 11.161270 16.691485 22.261017 28.306614 1.710000 37.450394 -0.266106 -0.181975 -0.000014 0.631810 560 46 Session_03 ETH-3 -4.000 26.000 5.742374 11.161270 16.679119 22.305357 28.306614 1.710000 37.450394 -0.278266 -0.138609 -0.000014 0.619771 561 47 Session_03 ETH-3 -4.000 26.000 5.742374 11.161270 16.663623 22.327286 28.306614 1.710000 37.450394 -0.293503 -0.117161 -0.000014 0.604685 562 48 Session_03 ETH-3 -4.000 26.000 5.742374 11.161270 16.678524 22.282103 28.306614 1.710000 37.450394 -0.278851 -0.161352 -0.000014 0.619192 563 49 Session_03 ETH-3 -4.000 26.000 5.742374 11.161270 16.666246 22.283361 28.306614 1.710000 37.450394 -0.290925 -0.160121 -0.000014 0.607238 564 50 Session_03 FOO -4.000 26.000 -0.840413 2.828738 1.309929 5.340249 4.665655 -5.000000 28.907344 -0.600546 -0.323413 -0.000006 0.300148 565 51 Session_03 FOO -4.000 26.000 -0.840413 2.828738 1.317548 5.334102 4.665655 -5.000000 28.907344 -0.592942 -0.329524 -0.000006 0.307676 566 52 Session_04 ETH-1 -4.000 26.000 6.018962 10.747026 16.136865 21.300298 27.780042 2.020000 37.024281 -0.692364 -0.302672 -0.000013 0.204033 567 53 Session_04 ETH-1 -4.000 26.000 6.018962 10.747026 16.133538 21.291260 27.780042 2.020000 37.024281 -0.695637 -0.311519 -0.000013 0.200762 568 54 Session_04 ETH-1 -4.000 26.000 6.018962 10.747026 16.139991 21.319865 27.780042 2.020000 37.024281 -0.689290 -0.283519 -0.000013 0.207107 569 55 Session_04 ETH-1 -4.000 26.000 6.018962 10.747026 16.145748 21.330075 27.780042 2.020000 37.024281 -0.683629 -0.273524 -0.000013 0.212766 570 56 Session_04 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.702989 -12.202762 -18.023381 -10.170000 19.875825 -0.686660 -0.289833 -0.000002 0.204507 571 57 Session_04 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.692830 -12.240287 -18.023381 -10.170000 19.875825 -0.676377 -0.327811 -0.000002 0.214786 572 58 Session_04 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.702899 -12.180291 -18.023381 -10.170000 19.875825 -0.686568 -0.267091 -0.000002 0.204598 573 59 Session_04 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.709282 -12.282257 -18.023381 -10.170000 19.875825 -0.693029 -0.370287 -0.000002 0.198140 574 60 Session_04 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.679330 -12.235994 -18.023381 -10.170000 19.875825 -0.662712 -0.323466 -0.000002 0.228446 575 61 Session_04 ETH-3 -4.000 26.000 5.742374 11.161270 16.695594 22.238663 28.306614 1.710000 37.450394 -0.262066 -0.203838 -0.000014 0.634200 576 62 Session_04 ETH-3 -4.000 26.000 5.742374 11.161270 16.663504 22.286354 28.306614 1.710000 37.450394 -0.293620 -0.157194 -0.000014 0.602656 577 63 Session_04 ETH-3 -4.000 26.000 5.742374 11.161270 16.666457 22.254290 28.306614 1.710000 37.450394 -0.290717 -0.188555 -0.000014 0.605558 578 64 Session_04 ETH-3 -4.000 26.000 5.742374 11.161270 16.666910 22.223232 28.306614 1.710000 37.450394 -0.290271 -0.218930 -0.000014 0.606004 579 65 Session_04 ETH-3 -4.000 26.000 5.742374 11.161270 16.679662 22.257256 28.306614 1.710000 37.450394 -0.277732 -0.185653 -0.000014 0.618539 580 66 Session_04 ETH-3 -4.000 26.000 5.742374 11.161270 16.676768 22.267680 28.306614 1.710000 37.450394 -0.280578 -0.175459 -0.000014 0.615693 581 67 Session_04 FOO -4.000 26.000 -0.840413 2.828738 1.307663 5.317330 4.665655 -5.000000 28.907344 -0.602808 -0.346202 -0.000006 0.290853 582 68 Session_04 FOO -4.000 26.000 -0.840413 2.828738 1.308562 5.331400 4.665655 -5.000000 28.907344 -0.601911 -0.332212 -0.000006 0.291749 583 ––– –––––––––– –––––– ––––––––––– –––––––––––– ––––––––– ––––––––– –––––––––– –––––––––– –––––––––– –––––––––– –––––––––– ––––––––– ––––––––– ––––––––– –––––––– 584 ``` 585 ''' 586 587 kwargs = locals().copy() 588 589 from numpy import random as nprandom 590 if seed: 591 rng = nprandom.default_rng(seed) 592 else: 593 rng = nprandom.default_rng() 594 595 N = sum([s['N'] for s in samples]) 596 errors47 = rng.normal(loc = 0, scale = 1, size = N) # generate random measurement errors 597 errors47 *= rD47 / stdev(errors47) # scale errors to rD47 598 errors48 = rng.normal(loc = 0, scale = 1, size = N) # generate random measurement errors 599 errors48 *= rD48 / stdev(errors48) # scale errors to rD48 600 601 k = 0 602 out = [] 603 for s in samples: 604 kw = {} 605 kw['sample'] = s['Sample'] 606 kw = { 607 **kw, 608 **{var: kwargs[var] 609 for var in [ 610 'd13Cwg_VPDB', 'd18Owg_VSMOW', 'ALPHA_18O_ACID_REACTION', 611 'Nominal_D47', 'Nominal_D48', 'Nominal_d13C_VPDB', 'Nominal_d18O_VPDB', 612 'R13_VPDB', 'R17_VSMOW', 'R18_VSMOW', 'LAMBDA_17', 'R18_VPDB', 613 'a47', 'b47', 'c47', 'a48', 'b48', 'c48', 614 ] 615 if kwargs[var] is not None}, 616 **{var: s[var] 617 for var in ['d13C_VPDB', 'd18O_VPDB', 'D47', 'D48', 'D49', 'D17O'] 618 if var in s}, 619 } 620 621 sN = s['N'] 622 while sN: 623 out.append(simulate_single_analysis(**kw)) 624 out[-1]['d47'] += errors47[k] * a47 625 out[-1]['d48'] += errors48[k] * a48 626 sN -= 1 627 k += 1 628 629 if session is not None: 630 for r in out: 631 r['Session'] = session 632 return out 633 634def table_of_samples( 635 data47 = None, 636 data48 = None, 637 dir = 'output', 638 filename = None, 639 save_to_file = True, 640 print_out = True, 641 output = None, 642 ): 643 ''' 644 Print out, save to disk and/or return a combined table of samples 645 for a pair of `D47data` and `D48data` objects. 646 647 **Parameters** 648 649 + `data47`: `D47data` instance 650 + `data48`: `D48data` instance 651 + `dir`: the directory in which to save the table 652 + `filename`: the name to the csv file to write to 653 + `save_to_file`: whether to save the table to disk 654 + `print_out`: whether to print out the table 655 + `output`: if set to `'pretty'`: return a pretty text table (see `pretty_table()`); 656 if set to `'raw'`: return a list of list of strings 657 (e.g., `[['header1', 'header2'], ['0.1', '0.2']]`) 658 ''' 659 if data47 is None: 660 if data48 is None: 661 raise TypeError("Arguments must include at least one D47data() or D48data() instance.") 662 else: 663 return data48.table_of_samples( 664 dir = dir, 665 filename = filename, 666 save_to_file = save_to_file, 667 print_out = print_out, 668 output = output 669 ) 670 else: 671 if data48 is None: 672 return data47.table_of_samples( 673 dir = dir, 674 filename = filename, 675 save_to_file = save_to_file, 676 print_out = print_out, 677 output = output 678 ) 679 else: 680 out47 = data47.table_of_samples(save_to_file = False, print_out = False, output = 'raw') 681 out48 = data48.table_of_samples(save_to_file = False, print_out = False, output = 'raw') 682 out = transpose_table(transpose_table(out47) + transpose_table(out48)[4:]) 683 684 if save_to_file: 685 if not os.path.exists(dir): 686 os.makedirs(dir) 687 if filename is None: 688 filename = f'D47D48_samples.csv' 689 with open(f'{dir}/{filename}', 'w') as fid: 690 fid.write(make_csv(out)) 691 if print_out: 692 print('\n'+pretty_table(out)) 693 if output == 'raw': 694 return out 695 elif output == 'pretty': 696 return pretty_table(out) 697 698 699def table_of_sessions( 700 data47 = None, 701 data48 = None, 702 dir = 'output', 703 filename = None, 704 save_to_file = True, 705 print_out = True, 706 output = None, 707 ): 708 ''' 709 Print out, save to disk and/or return a combined table of sessions 710 for a pair of `D47data` and `D48data` objects. 711 ***Only applicable if the sessions in `data47` and those in `data48` 712 consist of the exact same sets of analyses.*** 713 714 **Parameters** 715 716 + `data47`: `D47data` instance 717 + `data48`: `D48data` instance 718 + `dir`: the directory in which to save the table 719 + `filename`: the name to the csv file to write to 720 + `save_to_file`: whether to save the table to disk 721 + `print_out`: whether to print out the table 722 + `output`: if set to `'pretty'`: return a pretty text table (see `pretty_table()`); 723 if set to `'raw'`: return a list of list of strings 724 (e.g., `[['header1', 'header2'], ['0.1', '0.2']]`) 725 ''' 726 if data47 is None: 727 if data48 is None: 728 raise TypeError("Arguments must include at least one D47data() or D48data() instance.") 729 else: 730 return data48.table_of_sessions( 731 dir = dir, 732 filename = filename, 733 save_to_file = save_to_file, 734 print_out = print_out, 735 output = output 736 ) 737 else: 738 if data48 is None: 739 return data47.table_of_sessions( 740 dir = dir, 741 filename = filename, 742 save_to_file = save_to_file, 743 print_out = print_out, 744 output = output 745 ) 746 else: 747 out47 = data47.table_of_sessions(save_to_file = False, print_out = False, output = 'raw') 748 out48 = data48.table_of_sessions(save_to_file = False, print_out = False, output = 'raw') 749 for k,x in enumerate(out47[0]): 750 if k>7: 751 out47[0][k] = out47[0][k].replace('a', 'a_47').replace('b', 'b_47').replace('c', 'c_47') 752 out48[0][k] = out48[0][k].replace('a', 'a_48').replace('b', 'b_48').replace('c', 'c_48') 753 out = transpose_table(transpose_table(out47) + transpose_table(out48)[7:]) 754 755 if save_to_file: 756 if not os.path.exists(dir): 757 os.makedirs(dir) 758 if filename is None: 759 filename = f'D47D48_sessions.csv' 760 with open(f'{dir}/{filename}', 'w') as fid: 761 fid.write(make_csv(out)) 762 if print_out: 763 print('\n'+pretty_table(out)) 764 if output == 'raw': 765 return out 766 elif output == 'pretty': 767 return pretty_table(out) 768 769 770def table_of_analyses( 771 data47 = None, 772 data48 = None, 773 dir = 'output', 774 filename = None, 775 save_to_file = True, 776 print_out = True, 777 output = None, 778 ): 779 ''' 780 Print out, save to disk and/or return a combined table of analyses 781 for a pair of `D47data` and `D48data` objects. 782 783 If the sessions in `data47` and those in `data48` do not consist of 784 the exact same sets of analyses, the table will have two columns 785 `Session_47` and `Session_48` instead of a single `Session` column. 786 787 **Parameters** 788 789 + `data47`: `D47data` instance 790 + `data48`: `D48data` instance 791 + `dir`: the directory in which to save the table 792 + `filename`: the name to the csv file to write to 793 + `save_to_file`: whether to save the table to disk 794 + `print_out`: whether to print out the table 795 + `output`: if set to `'pretty'`: return a pretty text table (see `pretty_table()`); 796 if set to `'raw'`: return a list of list of strings 797 (e.g., `[['header1', 'header2'], ['0.1', '0.2']]`) 798 ''' 799 if data47 is None: 800 if data48 is None: 801 raise TypeError("Arguments must include at least one D47data() or D48data() instance.") 802 else: 803 return data48.table_of_analyses( 804 dir = dir, 805 filename = filename, 806 save_to_file = save_to_file, 807 print_out = print_out, 808 output = output 809 ) 810 else: 811 if data48 is None: 812 return data47.table_of_analyses( 813 dir = dir, 814 filename = filename, 815 save_to_file = save_to_file, 816 print_out = print_out, 817 output = output 818 ) 819 else: 820 out47 = data47.table_of_analyses(save_to_file = False, print_out = False, output = 'raw') 821 out48 = data48.table_of_analyses(save_to_file = False, print_out = False, output = 'raw') 822 823 if [l[1] for l in out47[1:]] == [l[1] for l in out48[1:]]: # if sessions are identical 824 out = transpose_table(transpose_table(out47) + transpose_table(out48)[-1:]) 825 else: 826 out47[0][1] = 'Session_47' 827 out48[0][1] = 'Session_48' 828 out47 = transpose_table(out47) 829 out48 = transpose_table(out48) 830 out = transpose_table(out47[:2] + out48[1:2] + out47[2:] + out48[-1:]) 831 832 if save_to_file: 833 if not os.path.exists(dir): 834 os.makedirs(dir) 835 if filename is None: 836 filename = f'D47D48_sessions.csv' 837 with open(f'{dir}/{filename}', 'w') as fid: 838 fid.write(make_csv(out)) 839 if print_out: 840 print('\n'+pretty_table(out)) 841 if output == 'raw': 842 return out 843 elif output == 'pretty': 844 return pretty_table(out) 845 846 847class D4xdata(list): 848 ''' 849 Store and process data for a large set of Δ47 and/or Δ48 850 analyses, usually comprising more than one analytical session. 851 ''' 852 853 ### 17O CORRECTION PARAMETERS 854 R13_VPDB = 0.01118 # (Chang & Li, 1990) 855 ''' 856 Absolute (13C/12C) ratio of VPDB. 857 By default equal to 0.01118 ([Chang & Li, 1990](http://www.cnki.com.cn/Article/CJFDTotal-JXTW199004006.htm)) 858 ''' 859 860 R18_VSMOW = 0.0020052 # (Baertschi, 1976) 861 ''' 862 Absolute (18O/16C) ratio of VSMOW. 863 By default equal to 0.0020052 ([Baertschi, 1976](https://doi.org/10.1016/0012-821X(76)90115-1)) 864 ''' 865 866 LAMBDA_17 = 0.528 # (Barkan & Luz, 2005) 867 ''' 868 Mass-dependent exponent for triple oxygen isotopes. 869 By default equal to 0.528 ([Barkan & Luz, 2005](https://doi.org/10.1002/rcm.2250)) 870 ''' 871 872 R17_VSMOW = 0.00038475 # (Assonov & Brenninkmeijer, 2003, rescaled to R13_VPDB) 873 ''' 874 Absolute (17O/16C) ratio of VSMOW. 875 By default equal to 0.00038475 876 ([Assonov & Brenninkmeijer, 2003](https://dx.doi.org/10.1002/rcm.1011), 877 rescaled to `R13_VPDB`) 878 ''' 879 880 R18_VPDB = R18_VSMOW * 1.03092 881 ''' 882 Absolute (18O/16C) ratio of VPDB. 883 By definition equal to `R18_VSMOW * 1.03092`. 884 ''' 885 886 R17_VPDB = R17_VSMOW * 1.03092 ** LAMBDA_17 887 ''' 888 Absolute (17O/16C) ratio of VPDB. 889 By definition equal to `R17_VSMOW * 1.03092 ** LAMBDA_17`. 890 ''' 891 892 LEVENE_REF_SAMPLE = 'ETH-3' 893 ''' 894 After the Δ4x standardization step, each sample is tested to 895 assess whether the Δ4x variance within all analyses for that 896 sample differs significantly from that observed for a given reference 897 sample (using [Levene's test](https://en.wikipedia.org/wiki/Levene%27s_test), 898 which yields a p-value corresponding to the null hypothesis that the 899 underlying variances are equal). 900 901 `LEVENE_REF_SAMPLE` (by default equal to `'ETH-3'`) specifies which 902 sample should be used as a reference for this test. 903 ''' 904 905 ALPHA_18O_ACID_REACTION = round(np.exp(3.59 / (90 + 273.15) - 1.79e-3), 6) # (Kim et al., 2007, calcite) 906 ''' 907 Specifies the 18O/16O fractionation factor generally applicable 908 to acid reactions in the dataset. Currently used by `D4xdata.wg()`, 909 `D4xdata.standardize_d13C`, and `D4xdata.standardize_d18O`. 910 911 By default equal to 1.008129 (calcite reacted at 90 °C, 912 [Kim et al., 2007](https://dx.doi.org/10.1016/j.chemgeo.2007.08.005)). 913 ''' 914 915 Nominal_d13C_VPDB = { 916 'ETH-1': 2.02, 917 'ETH-2': -10.17, 918 'ETH-3': 1.71, 919 } # (Bernasconi et al., 2018) 920 ''' 921 Nominal δ13C_VPDB values assigned to carbonate standards, used by 922 `D4xdata.standardize_d13C()`. 923 924 By default equal to `{'ETH-1': 2.02, 'ETH-2': -10.17, 'ETH-3': 1.71}` after 925 [Bernasconi et al. (2018)](https://doi.org/10.1029/2017GC007385). 926 ''' 927 928 Nominal_d18O_VPDB = { 929 'ETH-1': -2.19, 930 'ETH-2': -18.69, 931 'ETH-3': -1.78, 932 } # (Bernasconi et al., 2018) 933 ''' 934 Nominal δ18O_VPDB values assigned to carbonate standards, used by 935 `D4xdata.standardize_d18O()`. 936 937 By default equal to `{'ETH-1': -2.19, 'ETH-2': -18.69, 'ETH-3': -1.78}` after 938 [Bernasconi et al. (2018)](https://doi.org/10.1029/2017GC007385). 939 ''' 940 941 d13C_STANDARDIZATION_METHOD = '2pt' 942 ''' 943 Method by which to standardize δ13C values: 944 945 + `none`: do not apply any δ13C standardization. 946 + `'1pt'`: within each session, offset all initial δ13C values so as to 947 minimize the difference between final δ13C_VPDB values and 948 `Nominal_d13C_VPDB` (averaged over all analyses for which `Nominal_d13C_VPDB` is defined). 949 + `'2pt'`: within each session, apply a affine trasformation to all δ13C 950 values so as to minimize the difference between final δ13C_VPDB 951 values and `Nominal_d13C_VPDB` (averaged over all analyses for which `Nominal_d13C_VPDB` 952 is defined). 953 ''' 954 955 d18O_STANDARDIZATION_METHOD = '2pt' 956 ''' 957 Method by which to standardize δ18O values: 958 959 + `none`: do not apply any δ18O standardization. 960 + `'1pt'`: within each session, offset all initial δ18O values so as to 961 minimize the difference between final δ18O_VPDB values and 962 `Nominal_d18O_VPDB` (averaged over all analyses for which `Nominal_d18O_VPDB` is defined). 963 + `'2pt'`: within each session, apply a affine trasformation to all δ18O 964 values so as to minimize the difference between final δ18O_VPDB 965 values and `Nominal_d18O_VPDB` (averaged over all analyses for which `Nominal_d18O_VPDB` 966 is defined). 967 ''' 968 969 def __init__(self, l = [], mass = '47', logfile = '', session = 'mySession', verbose = False): 970 ''' 971 **Parameters** 972 973 + `l`: a list of dictionaries, with each dictionary including at least the keys 974 `Sample`, `d45`, `d46`, and `d47` or `d48`. 975 + `mass`: `'47'` or `'48'` 976 + `logfile`: if specified, write detailed logs to this file path when calling `D4xdata` methods. 977 + `session`: define session name for analyses without a `Session` key 978 + `verbose`: if `True`, print out detailed logs when calling `D4xdata` methods. 979 980 Returns a `D4xdata` object derived from `list`. 981 ''' 982 self._4x = mass 983 self.verbose = verbose 984 self.prefix = 'D4xdata' 985 self.logfile = logfile 986 list.__init__(self, l) 987 self.Nf = None 988 self.repeatability = {} 989 self.refresh(session = session) 990 991 992 def make_verbal(oldfun): 993 ''' 994 Decorator: allow temporarily changing `self.prefix` and overriding `self.verbose`. 995 ''' 996 @wraps(oldfun) 997 def newfun(*args, verbose = '', **kwargs): 998 myself = args[0] 999 oldprefix = myself.prefix 1000 myself.prefix = oldfun.__name__ 1001 if verbose != '': 1002 oldverbose = myself.verbose 1003 myself.verbose = verbose 1004 out = oldfun(*args, **kwargs) 1005 myself.prefix = oldprefix 1006 if verbose != '': 1007 myself.verbose = oldverbose 1008 return out 1009 return newfun 1010 1011 1012 def msg(self, txt): 1013 ''' 1014 Log a message to `self.logfile`, and print it out if `verbose = True` 1015 ''' 1016 self.log(txt) 1017 if self.verbose: 1018 print(f'{f"[{self.prefix}]":<16} {txt}') 1019 1020 1021 def vmsg(self, txt): 1022 ''' 1023 Log a message to `self.logfile` and print it out 1024 ''' 1025 self.log(txt) 1026 print(txt) 1027 1028 1029 def log(self, *txts): 1030 ''' 1031 Log a message to `self.logfile` 1032 ''' 1033 if self.logfile: 1034 with open(self.logfile, 'a') as fid: 1035 for txt in txts: 1036 fid.write(f'\n{dt.now().strftime("%Y-%m-%d %H:%M:%S")} {f"[{self.prefix}]":<16} {txt}') 1037 1038 1039 def refresh(self, session = 'mySession'): 1040 ''' 1041 Update `self.sessions`, `self.samples`, `self.anchors`, and `self.unknowns`. 1042 ''' 1043 self.fill_in_missing_info(session = session) 1044 self.refresh_sessions() 1045 self.refresh_samples() 1046 1047 1048 def refresh_sessions(self): 1049 ''' 1050 Update `self.sessions` and set `scrambling_drift`, `slope_drift`, and `wg_drift` 1051 to `False` for all sessions. 1052 ''' 1053 self.sessions = { 1054 s: {'data': [r for r in self if r['Session'] == s]} 1055 for s in sorted({r['Session'] for r in self}) 1056 } 1057 for s in self.sessions: 1058 self.sessions[s]['scrambling_drift'] = False 1059 self.sessions[s]['slope_drift'] = False 1060 self.sessions[s]['wg_drift'] = False 1061 self.sessions[s]['d13C_standardization_method'] = self.d13C_STANDARDIZATION_METHOD 1062 self.sessions[s]['d18O_standardization_method'] = self.d18O_STANDARDIZATION_METHOD 1063 1064 1065 def refresh_samples(self): 1066 ''' 1067 Define `self.samples`, `self.anchors`, and `self.unknowns`. 1068 ''' 1069 self.samples = { 1070 s: {'data': [r for r in self if r['Sample'] == s]} 1071 for s in sorted({r['Sample'] for r in self}) 1072 } 1073 self.anchors = {s: self.samples[s] for s in self.samples if s in self.Nominal_D4x} 1074 self.unknowns = {s: self.samples[s] for s in self.samples if s not in self.Nominal_D4x} 1075 1076 1077 def read(self, filename, sep = '', session = ''): 1078 ''' 1079 Read file in csv format to load data into a `D47data` object. 1080 1081 In the csv file, spaces before and after field separators (`','` by default) 1082 are optional. Each line corresponds to a single analysis. 1083 1084 The required fields are: 1085 1086 + `UID`: a unique identifier 1087 + `Session`: an identifier for the analytical session 1088 + `Sample`: a sample identifier 1089 + `d45`, `d46`, and at least one of `d47` or `d48`: the working-gas delta values 1090 1091 Independently known oxygen-17 anomalies may be provided as `D17O` (in ‰ relative to 1092 VSMOW, λ = `self.LAMBDA_17`), and are otherwise assumed to be zero. Working-gas deltas `d47`, `d48` 1093 and `d49` are optional, and set to NaN by default. 1094 1095 **Parameters** 1096 1097 + `fileneme`: the path of the file to read 1098 + `sep`: csv separator delimiting the fields 1099 + `session`: set `Session` field to this string for all analyses 1100 ''' 1101 with open(filename) as fid: 1102 self.input(fid.read(), sep = sep, session = session) 1103 1104 1105 def input(self, txt, sep = '', session = ''): 1106 ''' 1107 Read `txt` string in csv format to load analysis data into a `D47data` object. 1108 1109 In the csv string, spaces before and after field separators (`','` by default) 1110 are optional. Each line corresponds to a single analysis. 1111 1112 The required fields are: 1113 1114 + `UID`: a unique identifier 1115 + `Session`: an identifier for the analytical session 1116 + `Sample`: a sample identifier 1117 + `d45`, `d46`, and at least one of `d47` or `d48`: the working-gas delta values 1118 1119 Independently known oxygen-17 anomalies may be provided as `D17O` (in ‰ relative to 1120 VSMOW, λ = `self.LAMBDA_17`), and are otherwise assumed to be zero. Working-gas deltas `d47`, `d48` 1121 and `d49` are optional, and set to NaN by default. 1122 1123 **Parameters** 1124 1125 + `txt`: the csv string to read 1126 + `sep`: csv separator delimiting the fields. By default, use `,`, `;`, or `\t`, 1127 whichever appers most often in `txt`. 1128 + `session`: set `Session` field to this string for all analyses 1129 ''' 1130 if sep == '': 1131 sep = sorted(',;\t', key = lambda x: - txt.count(x))[0] 1132 txt = [[x.strip() for x in l.split(sep)] for l in txt.splitlines() if l.strip()] 1133 data = [{k: v if k in ['UID', 'Session', 'Sample'] else smart_type(v) for k,v in zip(txt[0], l) if v != ''} for l in txt[1:]] 1134 1135 if session != '': 1136 for r in data: 1137 r['Session'] = session 1138 1139 self += data 1140 self.refresh() 1141 1142 1143 @make_verbal 1144 def wg(self, samples = None, a18_acid = None): 1145 ''' 1146 Compute bulk composition of the working gas for each session based on 1147 the carbonate standards defined in both `self.Nominal_d13C_VPDB` and 1148 `self.Nominal_d18O_VPDB`. 1149 ''' 1150 1151 self.msg('Computing WG composition:') 1152 1153 if a18_acid is None: 1154 a18_acid = self.ALPHA_18O_ACID_REACTION 1155 if samples is None: 1156 samples = [s for s in self.Nominal_d13C_VPDB if s in self.Nominal_d18O_VPDB] 1157 1158 assert a18_acid, f'Acid fractionation factor should not be zero.' 1159 1160 samples = [s for s in samples if s in self.Nominal_d13C_VPDB and s in self.Nominal_d18O_VPDB] 1161 R45R46_standards = {} 1162 for sample in samples: 1163 d13C_vpdb = self.Nominal_d13C_VPDB[sample] 1164 d18O_vpdb = self.Nominal_d18O_VPDB[sample] 1165 R13_s = self.R13_VPDB * (1 + d13C_vpdb / 1000) 1166 R17_s = self.R17_VPDB * ((1 + d18O_vpdb / 1000) * a18_acid) ** self.LAMBDA_17 1167 R18_s = self.R18_VPDB * (1 + d18O_vpdb / 1000) * a18_acid 1168 1169 C12_s = 1 / (1 + R13_s) 1170 C13_s = R13_s / (1 + R13_s) 1171 C16_s = 1 / (1 + R17_s + R18_s) 1172 C17_s = R17_s / (1 + R17_s + R18_s) 1173 C18_s = R18_s / (1 + R17_s + R18_s) 1174 1175 C626_s = C12_s * C16_s ** 2 1176 C627_s = 2 * C12_s * C16_s * C17_s 1177 C628_s = 2 * C12_s * C16_s * C18_s 1178 C636_s = C13_s * C16_s ** 2 1179 C637_s = 2 * C13_s * C16_s * C17_s 1180 C727_s = C12_s * C17_s ** 2 1181 1182 R45_s = (C627_s + C636_s) / C626_s 1183 R46_s = (C628_s + C637_s + C727_s) / C626_s 1184 R45R46_standards[sample] = (R45_s, R46_s) 1185 1186 for s in self.sessions: 1187 db = [r for r in self.sessions[s]['data'] if r['Sample'] in samples] 1188 assert db, f'No sample from {samples} found in session "{s}".' 1189# dbsamples = sorted({r['Sample'] for r in db}) 1190 1191 X = [r['d45'] for r in db] 1192 Y = [R45R46_standards[r['Sample']][0] for r in db] 1193 x1, x2 = np.min(X), np.max(X) 1194 1195 if x1 < x2: 1196 wgcoord = x1/(x1-x2) 1197 else: 1198 wgcoord = 999 1199 1200 if wgcoord < -.5 or wgcoord > 1.5: 1201 # unreasonable to extrapolate to d45 = 0 1202 R45_wg = np.mean([y/(1+x/1000) for x,y in zip(X,Y)]) 1203 else : 1204 # d45 = 0 is reasonably well bracketed 1205 R45_wg = np.polyfit(X, Y, 1)[1] 1206 1207 X = [r['d46'] for r in db] 1208 Y = [R45R46_standards[r['Sample']][1] for r in db] 1209 x1, x2 = np.min(X), np.max(X) 1210 1211 if x1 < x2: 1212 wgcoord = x1/(x1-x2) 1213 else: 1214 wgcoord = 999 1215 1216 if wgcoord < -.5 or wgcoord > 1.5: 1217 # unreasonable to extrapolate to d46 = 0 1218 R46_wg = np.mean([y/(1+x/1000) for x,y in zip(X,Y)]) 1219 else : 1220 # d46 = 0 is reasonably well bracketed 1221 R46_wg = np.polyfit(X, Y, 1)[1] 1222 1223 d13Cwg_VPDB, d18Owg_VSMOW = self.compute_bulk_delta(R45_wg, R46_wg) 1224 1225 self.msg(f'Session {s} WG: δ13C_VPDB = {d13Cwg_VPDB:.3f} δ18O_VSMOW = {d18Owg_VSMOW:.3f}') 1226 1227 self.sessions[s]['d13Cwg_VPDB'] = d13Cwg_VPDB 1228 self.sessions[s]['d18Owg_VSMOW'] = d18Owg_VSMOW 1229 for r in self.sessions[s]['data']: 1230 r['d13Cwg_VPDB'] = d13Cwg_VPDB 1231 r['d18Owg_VSMOW'] = d18Owg_VSMOW 1232 1233 1234 def compute_bulk_delta(self, R45, R46, D17O = 0): 1235 ''' 1236 Compute δ13C_VPDB and δ18O_VSMOW, 1237 by solving the generalized form of equation (17) from 1238 [Brand et al. (2010)](https://doi.org/10.1351/PAC-REP-09-01-05), 1239 assuming that δ18O_VSMOW is not too big (0 ± 50 ‰) and 1240 solving the corresponding second-order Taylor polynomial. 1241 (Appendix A of [Daëron et al., 2016](https://doi.org/10.1016/j.chemgeo.2016.08.014)) 1242 ''' 1243 1244 K = np.exp(D17O / 1000) * self.R17_VSMOW * self.R18_VSMOW ** -self.LAMBDA_17 1245 1246 A = -3 * K ** 2 * self.R18_VSMOW ** (2 * self.LAMBDA_17) 1247 B = 2 * K * R45 * self.R18_VSMOW ** self.LAMBDA_17 1248 C = 2 * self.R18_VSMOW 1249 D = -R46 1250 1251 aa = A * self.LAMBDA_17 * (2 * self.LAMBDA_17 - 1) + B * self.LAMBDA_17 * (self.LAMBDA_17 - 1) / 2 1252 bb = 2 * A * self.LAMBDA_17 + B * self.LAMBDA_17 + C 1253 cc = A + B + C + D 1254 1255 d18O_VSMOW = 1000 * (-bb + (bb ** 2 - 4 * aa * cc) ** .5) / (2 * aa) 1256 1257 R18 = (1 + d18O_VSMOW / 1000) * self.R18_VSMOW 1258 R17 = K * R18 ** self.LAMBDA_17 1259 R13 = R45 - 2 * R17 1260 1261 d13C_VPDB = 1000 * (R13 / self.R13_VPDB - 1) 1262 1263 return d13C_VPDB, d18O_VSMOW 1264 1265 1266 @make_verbal 1267 def crunch(self, verbose = ''): 1268 ''' 1269 Compute bulk composition and raw clumped isotope anomalies for all analyses. 1270 ''' 1271 for r in self: 1272 self.compute_bulk_and_clumping_deltas(r) 1273 self.standardize_d13C() 1274 self.standardize_d18O() 1275 self.msg(f"Crunched {len(self)} analyses.") 1276 1277 1278 def fill_in_missing_info(self, session = 'mySession'): 1279 ''' 1280 Fill in optional fields with default values 1281 ''' 1282 for i,r in enumerate(self): 1283 if 'D17O' not in r: 1284 r['D17O'] = 0. 1285 if 'UID' not in r: 1286 r['UID'] = f'{i+1}' 1287 if 'Session' not in r: 1288 r['Session'] = session 1289 for k in ['d47', 'd48', 'd49']: 1290 if k not in r: 1291 r[k] = np.nan 1292 1293 1294 def standardize_d13C(self): 1295 ''' 1296 Perform δ13C standadization within each session `s` according to 1297 `self.sessions[s]['d13C_standardization_method']`, which is defined by default 1298 by `D47data.refresh_sessions()`as equal to `self.d13C_STANDARDIZATION_METHOD`, but 1299 may be redefined abitrarily at a later stage. 1300 ''' 1301 for s in self.sessions: 1302 if self.sessions[s]['d13C_standardization_method'] in ['1pt', '2pt']: 1303 XY = [(r['d13C_VPDB'], self.Nominal_d13C_VPDB[r['Sample']]) for r in self.sessions[s]['data'] if r['Sample'] in self.Nominal_d13C_VPDB] 1304 X,Y = zip(*XY) 1305 if self.sessions[s]['d13C_standardization_method'] == '1pt': 1306 offset = np.mean(Y) - np.mean(X) 1307 for r in self.sessions[s]['data']: 1308 r['d13C_VPDB'] += offset 1309 elif self.sessions[s]['d13C_standardization_method'] == '2pt': 1310 a,b = np.polyfit(X,Y,1) 1311 for r in self.sessions[s]['data']: 1312 r['d13C_VPDB'] = a * r['d13C_VPDB'] + b 1313 1314 def standardize_d18O(self): 1315 ''' 1316 Perform δ18O standadization within each session `s` according to 1317 `self.ALPHA_18O_ACID_REACTION` and `self.sessions[s]['d18O_standardization_method']`, 1318 which is defined by default by `D47data.refresh_sessions()`as equal to 1319 `self.d18O_STANDARDIZATION_METHOD`, but may be redefined abitrarily at a later stage. 1320 ''' 1321 for s in self.sessions: 1322 if self.sessions[s]['d18O_standardization_method'] in ['1pt', '2pt']: 1323 XY = [(r['d18O_VSMOW'], self.Nominal_d18O_VPDB[r['Sample']]) for r in self.sessions[s]['data'] if r['Sample'] in self.Nominal_d18O_VPDB] 1324 X,Y = zip(*XY) 1325 Y = [(1000+y) * self.R18_VPDB * self.ALPHA_18O_ACID_REACTION / self.R18_VSMOW - 1000 for y in Y] 1326 if self.sessions[s]['d18O_standardization_method'] == '1pt': 1327 offset = np.mean(Y) - np.mean(X) 1328 for r in self.sessions[s]['data']: 1329 r['d18O_VSMOW'] += offset 1330 elif self.sessions[s]['d18O_standardization_method'] == '2pt': 1331 a,b = np.polyfit(X,Y,1) 1332 for r in self.sessions[s]['data']: 1333 r['d18O_VSMOW'] = a * r['d18O_VSMOW'] + b 1334 1335 1336 def compute_bulk_and_clumping_deltas(self, r): 1337 ''' 1338 Compute δ13C_VPDB, δ18O_VSMOW, and raw Δ47, Δ48, Δ49 values for a single analysis `r`. 1339 ''' 1340 1341 # Compute working gas R13, R18, and isobar ratios 1342 R13_wg = self.R13_VPDB * (1 + r['d13Cwg_VPDB'] / 1000) 1343 R18_wg = self.R18_VSMOW * (1 + r['d18Owg_VSMOW'] / 1000) 1344 R45_wg, R46_wg, R47_wg, R48_wg, R49_wg = self.compute_isobar_ratios(R13_wg, R18_wg) 1345 1346 # Compute analyte isobar ratios 1347 R45 = (1 + r['d45'] / 1000) * R45_wg 1348 R46 = (1 + r['d46'] / 1000) * R46_wg 1349 R47 = (1 + r['d47'] / 1000) * R47_wg 1350 R48 = (1 + r['d48'] / 1000) * R48_wg 1351 R49 = (1 + r['d49'] / 1000) * R49_wg 1352 1353 r['d13C_VPDB'], r['d18O_VSMOW'] = self.compute_bulk_delta(R45, R46, D17O = r['D17O']) 1354 R13 = (1 + r['d13C_VPDB'] / 1000) * self.R13_VPDB 1355 R18 = (1 + r['d18O_VSMOW'] / 1000) * self.R18_VSMOW 1356 1357 # Compute stochastic isobar ratios of the analyte 1358 R45stoch, R46stoch, R47stoch, R48stoch, R49stoch = self.compute_isobar_ratios( 1359 R13, R18, D17O = r['D17O'] 1360 ) 1361 1362 # Check that R45/R45stoch and R46/R46stoch are undistinguishable from 1, 1363 # and raise a warning if the corresponding anomalies exceed 0.02 ppm. 1364 if (R45 / R45stoch - 1) > 5e-8: 1365 self.vmsg(f'This is unexpected: R45/R45stoch - 1 = {1e6 * (R45 / R45stoch - 1):.3f} ppm') 1366 if (R46 / R46stoch - 1) > 5e-8: 1367 self.vmsg(f'This is unexpected: R46/R46stoch - 1 = {1e6 * (R46 / R46stoch - 1):.3f} ppm') 1368 1369 # Compute raw clumped isotope anomalies 1370 r['D47raw'] = 1000 * (R47 / R47stoch - 1) 1371 r['D48raw'] = 1000 * (R48 / R48stoch - 1) 1372 r['D49raw'] = 1000 * (R49 / R49stoch - 1) 1373 1374 1375 def compute_isobar_ratios(self, R13, R18, D17O=0, D47=0, D48=0, D49=0): 1376 ''' 1377 Compute isobar ratios for a sample with isotopic ratios `R13` and `R18`, 1378 optionally accounting for non-zero values of Δ17O (`D17O`) and clumped isotope 1379 anomalies (`D47`, `D48`, `D49`), all expressed in permil. 1380 ''' 1381 1382 # Compute R17 1383 R17 = self.R17_VSMOW * np.exp(D17O / 1000) * (R18 / self.R18_VSMOW) ** self.LAMBDA_17 1384 1385 # Compute isotope concentrations 1386 C12 = (1 + R13) ** -1 1387 C13 = C12 * R13 1388 C16 = (1 + R17 + R18) ** -1 1389 C17 = C16 * R17 1390 C18 = C16 * R18 1391 1392 # Compute stochastic isotopologue concentrations 1393 C626 = C16 * C12 * C16 1394 C627 = C16 * C12 * C17 * 2 1395 C628 = C16 * C12 * C18 * 2 1396 C636 = C16 * C13 * C16 1397 C637 = C16 * C13 * C17 * 2 1398 C638 = C16 * C13 * C18 * 2 1399 C727 = C17 * C12 * C17 1400 C728 = C17 * C12 * C18 * 2 1401 C737 = C17 * C13 * C17 1402 C738 = C17 * C13 * C18 * 2 1403 C828 = C18 * C12 * C18 1404 C838 = C18 * C13 * C18 1405 1406 # Compute stochastic isobar ratios 1407 R45 = (C636 + C627) / C626 1408 R46 = (C628 + C637 + C727) / C626 1409 R47 = (C638 + C728 + C737) / C626 1410 R48 = (C738 + C828) / C626 1411 R49 = C838 / C626 1412 1413 # Account for stochastic anomalies 1414 R47 *= 1 + D47 / 1000 1415 R48 *= 1 + D48 / 1000 1416 R49 *= 1 + D49 / 1000 1417 1418 # Return isobar ratios 1419 return R45, R46, R47, R48, R49 1420 1421 1422 def split_samples(self, samples_to_split = 'all', grouping = 'by_session'): 1423 ''' 1424 Split unknown samples by UID (treat all analyses as different samples) 1425 or by session (treat analyses of a given sample in different sessions as 1426 different samples). 1427 1428 **Parameters** 1429 1430 + `samples_to_split`: a list of samples to split, e.g., `['IAEA-C1', 'IAEA-C2']` 1431 + `grouping`: `by_uid` | `by_session` 1432 ''' 1433 if samples_to_split == 'all': 1434 samples_to_split = [s for s in self.unknowns] 1435 gkeys = {'by_uid':'UID', 'by_session':'Session'} 1436 self.grouping = grouping.lower() 1437 if self.grouping in gkeys: 1438 gkey = gkeys[self.grouping] 1439 for r in self: 1440 if r['Sample'] in samples_to_split: 1441 r['Sample_original'] = r['Sample'] 1442 r['Sample'] = f"{r['Sample']}__{r[gkey]}" 1443 elif r['Sample'] in self.unknowns: 1444 r['Sample_original'] = r['Sample'] 1445 self.refresh_samples() 1446 1447 1448 def unsplit_samples(self, tables = False): 1449 ''' 1450 Reverse the effects of `D47data.split_samples()`. 1451 1452 This should only be used after `D4xdata.standardize()` with `method='pooled'`. 1453 1454 After `D4xdata.standardize()` with `method='indep_sessions'`, one should 1455 probably use `D4xdata.combine_samples()` instead to reverse the effects of 1456 `D47data.split_samples()` with `grouping='by_uid'`, or `w_avg()` to reverse the 1457 effects of `D47data.split_samples()` with `grouping='by_sessions'` (because in 1458 that case session-averaged Δ4x values are statistically independent). 1459 ''' 1460 unknowns_old = sorted({s for s in self.unknowns}) 1461 CM_old = self.standardization.covar[:,:] 1462 VD_old = self.standardization.params.valuesdict().copy() 1463 vars_old = self.standardization.var_names 1464 1465 unknowns_new = sorted({r['Sample_original'] for r in self if 'Sample_original' in r}) 1466 1467 Ns = len(vars_old) - len(unknowns_old) 1468 vars_new = vars_old[:Ns] + [f'D{self._4x}_{pf(u)}' for u in unknowns_new] 1469 VD_new = {k: VD_old[k] for k in vars_old[:Ns]} 1470 1471 W = np.zeros((len(vars_new), len(vars_old))) 1472 W[:Ns,:Ns] = np.eye(Ns) 1473 for u in unknowns_new: 1474 splits = sorted({r['Sample'] for r in self if 'Sample_original' in r and r['Sample_original'] == u}) 1475 if self.grouping == 'by_session': 1476 weights = [self.samples[s][f'SE_D{self._4x}']**-2 for s in splits] 1477 elif self.grouping == 'by_uid': 1478 weights = [1 for s in splits] 1479 sw = sum(weights) 1480 weights = [w/sw for w in weights] 1481 W[vars_new.index(f'D{self._4x}_{pf(u)}'),[vars_old.index(f'D{self._4x}_{pf(s)}') for s in splits]] = weights[:] 1482 1483 CM_new = W @ CM_old @ W.T 1484 V = W @ np.array([[VD_old[k]] for k in vars_old]) 1485 VD_new = {k:v[0] for k,v in zip(vars_new, V)} 1486 1487 self.standardization.covar = CM_new 1488 self.standardization.params.valuesdict = lambda : VD_new 1489 self.standardization.var_names = vars_new 1490 1491 for r in self: 1492 if r['Sample'] in self.unknowns: 1493 r['Sample_split'] = r['Sample'] 1494 r['Sample'] = r['Sample_original'] 1495 1496 self.refresh_samples() 1497 self.consolidate_samples() 1498 self.repeatabilities() 1499 1500 if tables: 1501 self.table_of_analyses() 1502 self.table_of_samples() 1503 1504 def assign_timestamps(self): 1505 ''' 1506 Assign a time field `t` of type `float` to each analysis. 1507 1508 If `TimeTag` is one of the data fields, `t` is equal within a given session 1509 to `TimeTag` minus the mean value of `TimeTag` for that session. 1510 Otherwise, `TimeTag` is by default equal to the index of each analysis 1511 in the dataset and `t` is defined as above. 1512 ''' 1513 for session in self.sessions: 1514 sdata = self.sessions[session]['data'] 1515 try: 1516 t0 = np.mean([r['TimeTag'] for r in sdata]) 1517 for r in sdata: 1518 r['t'] = r['TimeTag'] - t0 1519 except KeyError: 1520 t0 = (len(sdata)-1)/2 1521 for t,r in enumerate(sdata): 1522 r['t'] = t - t0 1523 1524 1525 def report(self): 1526 ''' 1527 Prints a report on the standardization fit. 1528 Only applicable after `D4xdata.standardize(method='pooled')`. 1529 ''' 1530 report_fit(self.standardization) 1531 1532 1533 def combine_samples(self, sample_groups): 1534 ''' 1535 Combine analyses of different samples to compute weighted average Δ4x 1536 and new error (co)variances corresponding to the groups defined by the `sample_groups` 1537 dictionary. 1538 1539 Caution: samples are weighted by number of replicate analyses, which is a 1540 reasonable default behavior but is not always optimal (e.g., in the case of strongly 1541 correlated analytical errors for one or more samples). 1542 1543 Returns a tuplet of: 1544 1545 + the list of group names 1546 + an array of the corresponding Δ4x values 1547 + the corresponding (co)variance matrix 1548 1549 **Parameters** 1550 1551 + `sample_groups`: a dictionary of the form: 1552 ```py 1553 {'group1': ['sample_1', 'sample_2'], 1554 'group2': ['sample_3', 'sample_4', 'sample_5']} 1555 ``` 1556 ''' 1557 1558 samples = [s for k in sorted(sample_groups.keys()) for s in sorted(sample_groups[k])] 1559 groups = sorted(sample_groups.keys()) 1560 group_total_weights = {k: sum([self.samples[s]['N'] for s in sample_groups[k]]) for k in groups} 1561 D4x_old = np.array([[self.samples[x][f'D{self._4x}']] for x in samples]) 1562 CM_old = np.array([[self.sample_D4x_covar(x,y) for x in samples] for y in samples]) 1563 W = np.array([ 1564 [self.samples[i]['N']/group_total_weights[j] if i in sample_groups[j] else 0 for i in samples] 1565 for j in groups]) 1566 D4x_new = W @ D4x_old 1567 CM_new = W @ CM_old @ W.T 1568 1569 return groups, D4x_new[:,0], CM_new 1570 1571 1572 @make_verbal 1573 def standardize(self, 1574 method = 'pooled', 1575 weighted_sessions = [], 1576 consolidate = True, 1577 consolidate_tables = False, 1578 consolidate_plots = False, 1579 constraints = {}, 1580 ): 1581 ''' 1582 Compute absolute Δ4x values for all replicate analyses and for sample averages. 1583 If `method` argument is set to `'pooled'`, the standardization processes all sessions 1584 in a single step, assuming that all samples (anchors and unknowns alike) are homogeneous, 1585 i.e. that their true Δ4x value does not change between sessions, 1586 ([Daëron, 2021](https://doi.org/10.1029/2020GC009592)). If `method` argument is set to 1587 `'indep_sessions'`, the standardization processes each session independently, based only 1588 on anchors analyses. 1589 ''' 1590 1591 self.standardization_method = method 1592 self.assign_timestamps() 1593 1594 if method == 'pooled': 1595 if weighted_sessions: 1596 for session_group in weighted_sessions: 1597 if self._4x == '47': 1598 X = D47data([r for r in self if r['Session'] in session_group]) 1599 elif self._4x == '48': 1600 X = D48data([r for r in self if r['Session'] in session_group]) 1601 X.Nominal_D4x = self.Nominal_D4x.copy() 1602 X.refresh() 1603 result = X.standardize(method = 'pooled', weighted_sessions = [], consolidate = False) 1604 w = np.sqrt(result.redchi) 1605 self.msg(f'Session group {session_group} MRSWD = {w:.4f}') 1606 for r in X: 1607 r[f'wD{self._4x}raw'] *= w 1608 else: 1609 self.msg(f'All D{self._4x}raw weights set to 1 ‰') 1610 for r in self: 1611 r[f'wD{self._4x}raw'] = 1. 1612 1613 params = Parameters() 1614 for k,session in enumerate(self.sessions): 1615 self.msg(f"Session {session}: scrambling_drift is {self.sessions[session]['scrambling_drift']}.") 1616 self.msg(f"Session {session}: slope_drift is {self.sessions[session]['slope_drift']}.") 1617 self.msg(f"Session {session}: wg_drift is {self.sessions[session]['wg_drift']}.") 1618 s = pf(session) 1619 params.add(f'a_{s}', value = 0.9) 1620 params.add(f'b_{s}', value = 0.) 1621 params.add(f'c_{s}', value = -0.9) 1622 params.add(f'a2_{s}', value = 0., vary = self.sessions[session]['scrambling_drift']) 1623 params.add(f'b2_{s}', value = 0., vary = self.sessions[session]['slope_drift']) 1624 params.add(f'c2_{s}', value = 0., vary = self.sessions[session]['wg_drift']) 1625 for sample in self.unknowns: 1626 params.add(f'D{self._4x}_{pf(sample)}', value = 0.5) 1627 1628 for k in constraints: 1629 params[k].expr = constraints[k] 1630 1631 def residuals(p): 1632 R = [] 1633 for r in self: 1634 session = pf(r['Session']) 1635 sample = pf(r['Sample']) 1636 if r['Sample'] in self.Nominal_D4x: 1637 R += [ ( 1638 r[f'D{self._4x}raw'] - ( 1639 p[f'a_{session}'] * self.Nominal_D4x[r['Sample']] 1640 + p[f'b_{session}'] * r[f'd{self._4x}'] 1641 + p[f'c_{session}'] 1642 + r['t'] * ( 1643 p[f'a2_{session}'] * self.Nominal_D4x[r['Sample']] 1644 + p[f'b2_{session}'] * r[f'd{self._4x}'] 1645 + p[f'c2_{session}'] 1646 ) 1647 ) 1648 ) / r[f'wD{self._4x}raw'] ] 1649 else: 1650 R += [ ( 1651 r[f'D{self._4x}raw'] - ( 1652 p[f'a_{session}'] * p[f'D{self._4x}_{sample}'] 1653 + p[f'b_{session}'] * r[f'd{self._4x}'] 1654 + p[f'c_{session}'] 1655 + r['t'] * ( 1656 p[f'a2_{session}'] * p[f'D{self._4x}_{sample}'] 1657 + p[f'b2_{session}'] * r[f'd{self._4x}'] 1658 + p[f'c2_{session}'] 1659 ) 1660 ) 1661 ) / r[f'wD{self._4x}raw'] ] 1662 return R 1663 1664 M = Minimizer(residuals, params) 1665 result = M.least_squares() 1666 self.Nf = result.nfree 1667 self.t95 = tstudent.ppf(1 - 0.05/2, self.Nf) 1668# if self.verbose: 1669# report_fit(result) 1670 1671 for r in self: 1672 s = pf(r["Session"]) 1673 a = result.params.valuesdict()[f'a_{s}'] 1674 b = result.params.valuesdict()[f'b_{s}'] 1675 c = result.params.valuesdict()[f'c_{s}'] 1676 a2 = result.params.valuesdict()[f'a2_{s}'] 1677 b2 = result.params.valuesdict()[f'b2_{s}'] 1678 c2 = result.params.valuesdict()[f'c2_{s}'] 1679 r[f'D{self._4x}'] = (r[f'D{self._4x}raw'] - c - b * r[f'd{self._4x}'] - c2 * r['t'] - b2 * r['t'] * r[f'd{self._4x}']) / (a + a2 * r['t']) 1680 1681 self.standardization = result 1682 1683 for session in self.sessions: 1684 self.sessions[session]['Np'] = 3 1685 for k in ['scrambling', 'slope', 'wg']: 1686 if self.sessions[session][f'{k}_drift']: 1687 self.sessions[session]['Np'] += 1 1688 1689 if consolidate: 1690 self.consolidate(tables = consolidate_tables, plots = consolidate_plots) 1691 return result 1692 1693 1694 elif method == 'indep_sessions': 1695 1696 if weighted_sessions: 1697 for session_group in weighted_sessions: 1698 X = D4xdata([r for r in self if r['Session'] in session_group], mass = self._4x) 1699 X.Nominal_D4x = self.Nominal_D4x.copy() 1700 X.refresh() 1701 # This is only done to assign r['wD47raw'] for r in X: 1702 X.standardize(method = method, weighted_sessions = [], consolidate = False) 1703 self.msg(f'D{self._4x}raw weights set to {1000*X[0][f"wD{self._4x}raw"]:.1f} ppm for sessions in {session_group}') 1704 else: 1705 self.msg('All weights set to 1 ‰') 1706 for r in self: 1707 r[f'wD{self._4x}raw'] = 1 1708 1709 for session in self.sessions: 1710 s = self.sessions[session] 1711 p_names = ['a', 'b', 'c', 'a2', 'b2', 'c2'] 1712 p_active = [True, True, True, s['scrambling_drift'], s['slope_drift'], s['wg_drift']] 1713 s['Np'] = sum(p_active) 1714 sdata = s['data'] 1715 1716 A = np.array([ 1717 [ 1718 self.Nominal_D4x[r['Sample']] / r[f'wD{self._4x}raw'], 1719 r[f'd{self._4x}'] / r[f'wD{self._4x}raw'], 1720 1 / r[f'wD{self._4x}raw'], 1721 self.Nominal_D4x[r['Sample']] * r['t'] / r[f'wD{self._4x}raw'], 1722 r[f'd{self._4x}'] * r['t'] / r[f'wD{self._4x}raw'], 1723 r['t'] / r[f'wD{self._4x}raw'] 1724 ] 1725 for r in sdata if r['Sample'] in self.anchors 1726 ])[:,p_active] # only keep columns for the active parameters 1727 Y = np.array([[r[f'D{self._4x}raw'] / r[f'wD{self._4x}raw']] for r in sdata if r['Sample'] in self.anchors]) 1728 s['Na'] = Y.size 1729 CM = linalg.inv(A.T @ A) 1730 bf = (CM @ A.T @ Y).T[0,:] 1731 k = 0 1732 for n,a in zip(p_names, p_active): 1733 if a: 1734 s[n] = bf[k] 1735# self.msg(f'{n} = {bf[k]}') 1736 k += 1 1737 else: 1738 s[n] = 0. 1739# self.msg(f'{n} = 0.0') 1740 1741 for r in sdata : 1742 a, b, c, a2, b2, c2 = s['a'], s['b'], s['c'], s['a2'], s['b2'], s['c2'] 1743 r[f'D{self._4x}'] = (r[f'D{self._4x}raw'] - c - b * r[f'd{self._4x}'] - c2 * r['t'] - b2 * r['t'] * r[f'd{self._4x}']) / (a + a2 * r['t']) 1744 r[f'wD{self._4x}'] = r[f'wD{self._4x}raw'] / (a + a2 * r['t']) 1745 1746 s['CM'] = np.zeros((6,6)) 1747 i = 0 1748 k_active = [j for j,a in enumerate(p_active) if a] 1749 for j,a in enumerate(p_active): 1750 if a: 1751 s['CM'][j,k_active] = CM[i,:] 1752 i += 1 1753 1754 if not weighted_sessions: 1755 w = self.rmswd()['rmswd'] 1756 for r in self: 1757 r[f'wD{self._4x}'] *= w 1758 r[f'wD{self._4x}raw'] *= w 1759 for session in self.sessions: 1760 self.sessions[session]['CM'] *= w**2 1761 1762 for session in self.sessions: 1763 s = self.sessions[session] 1764 s['SE_a'] = s['CM'][0,0]**.5 1765 s['SE_b'] = s['CM'][1,1]**.5 1766 s['SE_c'] = s['CM'][2,2]**.5 1767 s['SE_a2'] = s['CM'][3,3]**.5 1768 s['SE_b2'] = s['CM'][4,4]**.5 1769 s['SE_c2'] = s['CM'][5,5]**.5 1770 1771 if not weighted_sessions: 1772 self.Nf = len(self) - len(self.unknowns) - np.sum([self.sessions[s]['Np'] for s in self.sessions]) 1773 else: 1774 self.Nf = 0 1775 for sg in weighted_sessions: 1776 self.Nf += self.rmswd(sessions = sg)['Nf'] 1777 1778 self.t95 = tstudent.ppf(1 - 0.05/2, self.Nf) 1779 1780 avgD4x = { 1781 sample: np.mean([r[f'D{self._4x}'] for r in self if r['Sample'] == sample]) 1782 for sample in self.samples 1783 } 1784 chi2 = np.sum([(r[f'D{self._4x}'] - avgD4x[r['Sample']])**2 for r in self]) 1785 rD4x = (chi2/self.Nf)**.5 1786 self.repeatability[f'sigma_{self._4x}'] = rD4x 1787 1788 if consolidate: 1789 self.consolidate(tables = consolidate_tables, plots = consolidate_plots) 1790 1791 1792 def standardization_error(self, session, d4x, D4x, t = 0): 1793 ''' 1794 Compute standardization error for a given session and 1795 (δ47, Δ47) composition. 1796 ''' 1797 a = self.sessions[session]['a'] 1798 b = self.sessions[session]['b'] 1799 c = self.sessions[session]['c'] 1800 a2 = self.sessions[session]['a2'] 1801 b2 = self.sessions[session]['b2'] 1802 c2 = self.sessions[session]['c2'] 1803 CM = self.sessions[session]['CM'] 1804 1805 x, y = D4x, d4x 1806 z = a * x + b * y + c + a2 * x * t + b2 * y * t + c2 * t 1807# x = (z - b*y - b2*y*t - c - c2*t) / (a+a2*t) 1808 dxdy = -(b+b2*t) / (a+a2*t) 1809 dxdz = 1. / (a+a2*t) 1810 dxda = -x / (a+a2*t) 1811 dxdb = -y / (a+a2*t) 1812 dxdc = -1. / (a+a2*t) 1813 dxda2 = -x * a2 / (a+a2*t) 1814 dxdb2 = -y * t / (a+a2*t) 1815 dxdc2 = -t / (a+a2*t) 1816 V = np.array([dxda, dxdb, dxdc, dxda2, dxdb2, dxdc2]) 1817 sx = (V @ CM @ V.T) ** .5 1818 return sx 1819 1820 1821 @make_verbal 1822 def summary(self, 1823 dir = 'output', 1824 filename = None, 1825 save_to_file = True, 1826 print_out = True, 1827 ): 1828 ''' 1829 Print out an/or save to disk a summary of the standardization results. 1830 1831 **Parameters** 1832 1833 + `dir`: the directory in which to save the table 1834 + `filename`: the name to the csv file to write to 1835 + `save_to_file`: whether to save the table to disk 1836 + `print_out`: whether to print out the table 1837 ''' 1838 1839 out = [] 1840 out += [['N samples (anchors + unknowns)', f"{len(self.samples)} ({len(self.anchors)} + {len(self.unknowns)})"]] 1841 out += [['N analyses (anchors + unknowns)', f"{len(self)} ({len([r for r in self if r['Sample'] in self.anchors])} + {len([r for r in self if r['Sample'] in self.unknowns])})"]] 1842 out += [['Repeatability of δ13C_VPDB', f"{1000 * self.repeatability['r_d13C_VPDB']:.1f} ppm"]] 1843 out += [['Repeatability of δ18O_VSMOW', f"{1000 * self.repeatability['r_d18O_VSMOW']:.1f} ppm"]] 1844 out += [[f'Repeatability of Δ{self._4x} (anchors)', f"{1000 * self.repeatability[f'r_D{self._4x}a']:.1f} ppm"]] 1845 out += [[f'Repeatability of Δ{self._4x} (unknowns)', f"{1000 * self.repeatability[f'r_D{self._4x}u']:.1f} ppm"]] 1846 out += [[f'Repeatability of Δ{self._4x} (all)', f"{1000 * self.repeatability[f'r_D{self._4x}']:.1f} ppm"]] 1847 out += [['Model degrees of freedom', f"{self.Nf}"]] 1848 out += [['Student\'s 95% t-factor', f"{self.t95:.2f}"]] 1849 out += [['Standardization method', self.standardization_method]] 1850 1851 if save_to_file: 1852 if not os.path.exists(dir): 1853 os.makedirs(dir) 1854 if filename is None: 1855 filename = f'D{self._4x}_summary.csv' 1856 with open(f'{dir}/{filename}', 'w') as fid: 1857 fid.write(make_csv(out)) 1858 if print_out: 1859 self.msg('\n' + pretty_table(out, header = 0)) 1860 1861 1862 @make_verbal 1863 def table_of_sessions(self, 1864 dir = 'output', 1865 filename = None, 1866 save_to_file = True, 1867 print_out = True, 1868 output = None, 1869 ): 1870 ''' 1871 Print out an/or save to disk a table of sessions. 1872 1873 **Parameters** 1874 1875 + `dir`: the directory in which to save the table 1876 + `filename`: the name to the csv file to write to 1877 + `save_to_file`: whether to save the table to disk 1878 + `print_out`: whether to print out the table 1879 + `output`: if set to `'pretty'`: return a pretty text table (see `pretty_table()`); 1880 if set to `'raw'`: return a list of list of strings 1881 (e.g., `[['header1', 'header2'], ['0.1', '0.2']]`) 1882 ''' 1883 include_a2 = any([self.sessions[session]['scrambling_drift'] for session in self.sessions]) 1884 include_b2 = any([self.sessions[session]['slope_drift'] for session in self.sessions]) 1885 include_c2 = any([self.sessions[session]['wg_drift'] for session in self.sessions]) 1886 1887 out = [['Session','Na','Nu','d13Cwg_VPDB','d18Owg_VSMOW','r_d13C','r_d18O',f'r_D{self._4x}','a ± SE','1e3 x b ± SE','c ± SE']] 1888 if include_a2: 1889 out[-1] += ['a2 ± SE'] 1890 if include_b2: 1891 out[-1] += ['b2 ± SE'] 1892 if include_c2: 1893 out[-1] += ['c2 ± SE'] 1894 for session in self.sessions: 1895 out += [[ 1896 session, 1897 f"{self.sessions[session]['Na']}", 1898 f"{self.sessions[session]['Nu']}", 1899 f"{self.sessions[session]['d13Cwg_VPDB']:.3f}", 1900 f"{self.sessions[session]['d18Owg_VSMOW']:.3f}", 1901 f"{self.sessions[session]['r_d13C_VPDB']:.4f}", 1902 f"{self.sessions[session]['r_d18O_VSMOW']:.4f}", 1903 f"{self.sessions[session][f'r_D{self._4x}']:.4f}", 1904 f"{self.sessions[session]['a']:.3f} ± {self.sessions[session]['SE_a']:.3f}", 1905 f"{1e3*self.sessions[session]['b']:.3f} ± {1e3*self.sessions[session]['SE_b']:.3f}", 1906 f"{self.sessions[session]['c']:.3f} ± {self.sessions[session]['SE_c']:.3f}", 1907 ]] 1908 if include_a2: 1909 if self.sessions[session]['scrambling_drift']: 1910 out[-1] += [f"{self.sessions[session]['a2']:.1e} ± {self.sessions[session]['SE_a2']:.1e}"] 1911 else: 1912 out[-1] += [''] 1913 if include_b2: 1914 if self.sessions[session]['slope_drift']: 1915 out[-1] += [f"{self.sessions[session]['b2']:.1e} ± {self.sessions[session]['SE_b2']:.1e}"] 1916 else: 1917 out[-1] += [''] 1918 if include_c2: 1919 if self.sessions[session]['wg_drift']: 1920 out[-1] += [f"{self.sessions[session]['c2']:.1e} ± {self.sessions[session]['SE_c2']:.1e}"] 1921 else: 1922 out[-1] += [''] 1923 1924 if save_to_file: 1925 if not os.path.exists(dir): 1926 os.makedirs(dir) 1927 if filename is None: 1928 filename = f'D{self._4x}_sessions.csv' 1929 with open(f'{dir}/{filename}', 'w') as fid: 1930 fid.write(make_csv(out)) 1931 if print_out: 1932 self.msg('\n' + pretty_table(out)) 1933 if output == 'raw': 1934 return out 1935 elif output == 'pretty': 1936 return pretty_table(out) 1937 1938 1939 @make_verbal 1940 def table_of_analyses( 1941 self, 1942 dir = 'output', 1943 filename = None, 1944 save_to_file = True, 1945 print_out = True, 1946 output = None, 1947 ): 1948 ''' 1949 Print out an/or save to disk a table of analyses. 1950 1951 **Parameters** 1952 1953 + `dir`: the directory in which to save the table 1954 + `filename`: the name to the csv file to write to 1955 + `save_to_file`: whether to save the table to disk 1956 + `print_out`: whether to print out the table 1957 + `output`: if set to `'pretty'`: return a pretty text table (see `pretty_table()`); 1958 if set to `'raw'`: return a list of list of strings 1959 (e.g., `[['header1', 'header2'], ['0.1', '0.2']]`) 1960 ''' 1961 1962 out = [['UID','Session','Sample']] 1963 extra_fields = [f for f in [('SampleMass','.2f'),('ColdFingerPressure','.1f'),('AcidReactionYield','.3f')] if f[0] in {k for r in self for k in r}] 1964 for f in extra_fields: 1965 out[-1] += [f[0]] 1966 out[-1] += ['d13Cwg_VPDB','d18Owg_VSMOW','d45','d46','d47','d48','d49','d13C_VPDB','d18O_VSMOW','D47raw','D48raw','D49raw',f'D{self._4x}'] 1967 for r in self: 1968 out += [[f"{r['UID']}",f"{r['Session']}",f"{r['Sample']}"]] 1969 for f in extra_fields: 1970 out[-1] += [f"{r[f[0]]:{f[1]}}"] 1971 out[-1] += [ 1972 f"{r['d13Cwg_VPDB']:.3f}", 1973 f"{r['d18Owg_VSMOW']:.3f}", 1974 f"{r['d45']:.6f}", 1975 f"{r['d46']:.6f}", 1976 f"{r['d47']:.6f}", 1977 f"{r['d48']:.6f}", 1978 f"{r['d49']:.6f}", 1979 f"{r['d13C_VPDB']:.6f}", 1980 f"{r['d18O_VSMOW']:.6f}", 1981 f"{r['D47raw']:.6f}", 1982 f"{r['D48raw']:.6f}", 1983 f"{r['D49raw']:.6f}", 1984 f"{r[f'D{self._4x}']:.6f}" 1985 ] 1986 if save_to_file: 1987 if not os.path.exists(dir): 1988 os.makedirs(dir) 1989 if filename is None: 1990 filename = f'D{self._4x}_analyses.csv' 1991 with open(f'{dir}/{filename}', 'w') as fid: 1992 fid.write(make_csv(out)) 1993 if print_out: 1994 self.msg('\n' + pretty_table(out)) 1995 return out 1996 1997 @make_verbal 1998 def covar_table( 1999 self, 2000 correl = False, 2001 dir = 'output', 2002 filename = None, 2003 save_to_file = True, 2004 print_out = True, 2005 output = None, 2006 ): 2007 ''' 2008 Print out, save to disk and/or return the variance-covariance matrix of D4x 2009 for all unknown samples. 2010 2011 **Parameters** 2012 2013 + `dir`: the directory in which to save the csv 2014 + `filename`: the name of the csv file to write to 2015 + `save_to_file`: whether to save the csv 2016 + `print_out`: whether to print out the matrix 2017 + `output`: if set to `'pretty'`: return a pretty text matrix (see `pretty_table()`); 2018 if set to `'raw'`: return a list of list of strings 2019 (e.g., `[['header1', 'header2'], ['0.1', '0.2']]`) 2020 ''' 2021 samples = sorted([u for u in self.unknowns]) 2022 out = [[''] + samples] 2023 for s1 in samples: 2024 out.append([s1]) 2025 for s2 in samples: 2026 if correl: 2027 out[-1].append(f'{self.sample_D4x_correl(s1, s2):.6f}') 2028 else: 2029 out[-1].append(f'{self.sample_D4x_covar(s1, s2):.8e}') 2030 2031 if save_to_file: 2032 if not os.path.exists(dir): 2033 os.makedirs(dir) 2034 if filename is None: 2035 if correl: 2036 filename = f'D{self._4x}_correl.csv' 2037 else: 2038 filename = f'D{self._4x}_covar.csv' 2039 with open(f'{dir}/{filename}', 'w') as fid: 2040 fid.write(make_csv(out)) 2041 if print_out: 2042 self.msg('\n'+pretty_table(out)) 2043 if output == 'raw': 2044 return out 2045 elif output == 'pretty': 2046 return pretty_table(out) 2047 2048 @make_verbal 2049 def table_of_samples( 2050 self, 2051 dir = 'output', 2052 filename = None, 2053 save_to_file = True, 2054 print_out = True, 2055 output = None, 2056 ): 2057 ''' 2058 Print out, save to disk and/or return a table of samples. 2059 2060 **Parameters** 2061 2062 + `dir`: the directory in which to save the csv 2063 + `filename`: the name of the csv file to write to 2064 + `save_to_file`: whether to save the csv 2065 + `print_out`: whether to print out the table 2066 + `output`: if set to `'pretty'`: return a pretty text table (see `pretty_table()`); 2067 if set to `'raw'`: return a list of list of strings 2068 (e.g., `[['header1', 'header2'], ['0.1', '0.2']]`) 2069 ''' 2070 2071 out = [['Sample','N','d13C_VPDB','d18O_VSMOW',f'D{self._4x}','SE','95% CL','SD','p_Levene']] 2072 for sample in self.anchors: 2073 out += [[ 2074 f"{sample}", 2075 f"{self.samples[sample]['N']}", 2076 f"{self.samples[sample]['d13C_VPDB']:.2f}", 2077 f"{self.samples[sample]['d18O_VSMOW']:.2f}", 2078 f"{self.samples[sample][f'D{self._4x}']:.4f}",'','', 2079 f"{self.samples[sample][f'SD_D{self._4x}']:.4f}" if self.samples[sample]['N'] > 1 else '', '' 2080 ]] 2081 for sample in self.unknowns: 2082 out += [[ 2083 f"{sample}", 2084 f"{self.samples[sample]['N']}", 2085 f"{self.samples[sample]['d13C_VPDB']:.2f}", 2086 f"{self.samples[sample]['d18O_VSMOW']:.2f}", 2087 f"{self.samples[sample][f'D{self._4x}']:.4f}", 2088 f"{self.samples[sample][f'SE_D{self._4x}']:.4f}", 2089 f"± {self.samples[sample][f'SE_D{self._4x}'] * self.t95:.4f}", 2090 f"{self.samples[sample][f'SD_D{self._4x}']:.4f}" if self.samples[sample]['N'] > 1 else '', 2091 f"{self.samples[sample]['p_Levene']:.3f}" if self.samples[sample]['N'] > 2 else '' 2092 ]] 2093 if save_to_file: 2094 if not os.path.exists(dir): 2095 os.makedirs(dir) 2096 if filename is None: 2097 filename = f'D{self._4x}_samples.csv' 2098 with open(f'{dir}/{filename}', 'w') as fid: 2099 fid.write(make_csv(out)) 2100 if print_out: 2101 self.msg('\n'+pretty_table(out)) 2102 if output == 'raw': 2103 return out 2104 elif output == 'pretty': 2105 return pretty_table(out) 2106 2107 2108 def plot_sessions(self, dir = 'output', figsize = (8,8)): 2109 ''' 2110 Generate session plots and save them to disk. 2111 2112 **Parameters** 2113 2114 + `dir`: the directory in which to save the plots 2115 + `figsize`: the width and height (in inches) of each plot 2116 ''' 2117 if not os.path.exists(dir): 2118 os.makedirs(dir) 2119 2120 for session in self.sessions: 2121 sp = self.plot_single_session(session, xylimits = 'constant') 2122 ppl.savefig(f'{dir}/D{self._4x}_plot_{session}.pdf') 2123 ppl.close(sp.fig) 2124 2125 2126 @make_verbal 2127 def consolidate_samples(self): 2128 ''' 2129 Compile various statistics for each sample. 2130 2131 For each anchor sample: 2132 2133 + `D47` or `D48`: the nominal Δ4x value for this anchor, specified by `self.Nominal_D4x` 2134 + `SE_D47` or `SE_D48`: set to zero by definition 2135 2136 For each unknown sample: 2137 2138 + `D47` or `D48`: the standardized Δ4x value for this unknown 2139 + `SE_D47` or `SE_D48`: the standard error of Δ4x for this unknown 2140 2141 For each anchor and unknown: 2142 2143 + `N`: the total number of analyses of this sample 2144 + `SD_D47` or `SD_D48`: the “sample” (in the statistical sense) standard deviation for this sample 2145 + `d13C_VPDB`: the average δ13C_VPDB value for this sample 2146 + `d18O_VSMOW`: the average δ18O_VSMOW value for this sample (as CO2) 2147 + `p_Levene`: the p-value from a [Levene test](https://en.wikipedia.org/wiki/Levene%27s_test) of equal 2148 variance, indicating whether the Δ4x repeatability this sample differs significantly from 2149 that observed for the reference sample specified by `self.LEVENE_REF_SAMPLE`. 2150 ''' 2151 D4x_ref_pop = [r[f'D{self._4x}'] for r in self.samples[self.LEVENE_REF_SAMPLE]['data']] 2152 for sample in self.samples: 2153 self.samples[sample]['N'] = len(self.samples[sample]['data']) 2154 if self.samples[sample]['N'] > 1: 2155 self.samples[sample][f'SD_D{self._4x}'] = stdev([r[f'D{self._4x}'] for r in self.samples[sample]['data']]) 2156 2157 self.samples[sample]['d13C_VPDB'] = np.mean([r['d13C_VPDB'] for r in self.samples[sample]['data']]) 2158 self.samples[sample]['d18O_VSMOW'] = np.mean([r['d18O_VSMOW'] for r in self.samples[sample]['data']]) 2159 2160 D4x_pop = [r[f'D{self._4x}'] for r in self.samples[sample]['data']] 2161 if len(D4x_pop) > 2: 2162 self.samples[sample]['p_Levene'] = levene(D4x_ref_pop, D4x_pop, center = 'median')[1] 2163 2164 if self.standardization_method == 'pooled': 2165 for sample in self.anchors: 2166 self.samples[sample][f'D{self._4x}'] = self.Nominal_D4x[sample] 2167 self.samples[sample][f'SE_D{self._4x}'] = 0. 2168 for sample in self.unknowns: 2169 self.samples[sample][f'D{self._4x}'] = self.standardization.params.valuesdict()[f'D{self._4x}_{pf(sample)}'] 2170 try: 2171 self.samples[sample][f'SE_D{self._4x}'] = self.sample_D4x_covar(sample)**.5 2172 except ValueError: 2173 # when `sample` is constrained by self.standardize(constraints = {...}), 2174 # it is no longer listed in self.standardization.var_names. 2175 # Temporary fix: define SE as zero for now 2176 self.samples[sample][f'SE_D4{self._4x}'] = 0. 2177 2178 elif self.standardization_method == 'indep_sessions': 2179 for sample in self.anchors: 2180 self.samples[sample][f'D{self._4x}'] = self.Nominal_D4x[sample] 2181 self.samples[sample][f'SE_D{self._4x}'] = 0. 2182 for sample in self.unknowns: 2183 self.msg(f'Consolidating sample {sample}') 2184 self.unknowns[sample][f'session_D{self._4x}'] = {} 2185 session_avg = [] 2186 for session in self.sessions: 2187 sdata = [r for r in self.sessions[session]['data'] if r['Sample'] == sample] 2188 if sdata: 2189 self.msg(f'{sample} found in session {session}') 2190 avg_D4x = np.mean([r[f'D{self._4x}'] for r in sdata]) 2191 avg_d4x = np.mean([r[f'd{self._4x}'] for r in sdata]) 2192 # !! TODO: sigma_s below does not account for temporal changes in standardization error 2193 sigma_s = self.standardization_error(session, avg_d4x, avg_D4x) 2194 sigma_u = sdata[0][f'wD{self._4x}raw'] / self.sessions[session]['a'] / len(sdata)**.5 2195 session_avg.append([avg_D4x, (sigma_u**2 + sigma_s**2)**.5]) 2196 self.unknowns[sample][f'session_D{self._4x}'][session] = session_avg[-1] 2197 self.samples[sample][f'D{self._4x}'], self.samples[sample][f'SE_D{self._4x}'] = w_avg(*zip(*session_avg)) 2198 weights = {s: self.unknowns[sample][f'session_D{self._4x}'][s][1]**-2 for s in self.unknowns[sample][f'session_D{self._4x}']} 2199 wsum = sum([weights[s] for s in weights]) 2200 for s in weights: 2201 self.unknowns[sample][f'session_D{self._4x}'][s] += [self.unknowns[sample][f'session_D{self._4x}'][s][1]**-2 / wsum] 2202 2203 2204 def consolidate_sessions(self): 2205 ''' 2206 Compute various statistics for each session. 2207 2208 + `Na`: Number of anchor analyses in the session 2209 + `Nu`: Number of unknown analyses in the session 2210 + `r_d13C_VPDB`: δ13C_VPDB repeatability of analyses within the session 2211 + `r_d18O_VSMOW`: δ18O_VSMOW repeatability of analyses within the session 2212 + `r_D47` or `r_D48`: Δ4x repeatability of analyses within the session 2213 + `a`: scrambling factor 2214 + `b`: compositional slope 2215 + `c`: WG offset 2216 + `SE_a`: Model stadard erorr of `a` 2217 + `SE_b`: Model stadard erorr of `b` 2218 + `SE_c`: Model stadard erorr of `c` 2219 + `scrambling_drift` (boolean): whether to allow a temporal drift in the scrambling factor (`a`) 2220 + `slope_drift` (boolean): whether to allow a temporal drift in the compositional slope (`b`) 2221 + `wg_drift` (boolean): whether to allow a temporal drift in the WG offset (`c`) 2222 + `a2`: scrambling factor drift 2223 + `b2`: compositional slope drift 2224 + `c2`: WG offset drift 2225 + `Np`: Number of standardization parameters to fit 2226 + `CM`: model covariance matrix for (`a`, `b`, `c`, `a2`, `b2`, `c2`) 2227 + `d13Cwg_VPDB`: δ13C_VPDB of WG 2228 + `d18Owg_VSMOW`: δ18O_VSMOW of WG 2229 ''' 2230 for session in self.sessions: 2231 if 'd13Cwg_VPDB' not in self.sessions[session]: 2232 self.sessions[session]['d13Cwg_VPDB'] = self.sessions[session]['data'][0]['d13Cwg_VPDB'] 2233 if 'd18Owg_VSMOW' not in self.sessions[session]: 2234 self.sessions[session]['d18Owg_VSMOW'] = self.sessions[session]['data'][0]['d18Owg_VSMOW'] 2235 self.sessions[session]['Na'] = len([r for r in self.sessions[session]['data'] if r['Sample'] in self.anchors]) 2236 self.sessions[session]['Nu'] = len([r for r in self.sessions[session]['data'] if r['Sample'] in self.unknowns]) 2237 2238 self.msg(f'Computing repeatabilities for session {session}') 2239 self.sessions[session]['r_d13C_VPDB'] = self.compute_r('d13C_VPDB', samples = 'anchors', sessions = [session]) 2240 self.sessions[session]['r_d18O_VSMOW'] = self.compute_r('d18O_VSMOW', samples = 'anchors', sessions = [session]) 2241 self.sessions[session][f'r_D{self._4x}'] = self.compute_r(f'D{self._4x}', sessions = [session]) 2242 2243 if self.standardization_method == 'pooled': 2244 for session in self.sessions: 2245 2246 self.sessions[session]['a'] = self.standardization.params.valuesdict()[f'a_{pf(session)}'] 2247 i = self.standardization.var_names.index(f'a_{pf(session)}') 2248 self.sessions[session]['SE_a'] = self.standardization.covar[i,i]**.5 2249 2250 self.sessions[session]['b'] = self.standardization.params.valuesdict()[f'b_{pf(session)}'] 2251 i = self.standardization.var_names.index(f'b_{pf(session)}') 2252 self.sessions[session]['SE_b'] = self.standardization.covar[i,i]**.5 2253 2254 self.sessions[session]['c'] = self.standardization.params.valuesdict()[f'c_{pf(session)}'] 2255 i = self.standardization.var_names.index(f'c_{pf(session)}') 2256 self.sessions[session]['SE_c'] = self.standardization.covar[i,i]**.5 2257 2258 self.sessions[session]['a2'] = self.standardization.params.valuesdict()[f'a2_{pf(session)}'] 2259 if self.sessions[session]['scrambling_drift']: 2260 i = self.standardization.var_names.index(f'a2_{pf(session)}') 2261 self.sessions[session]['SE_a2'] = self.standardization.covar[i,i]**.5 2262 else: 2263 self.sessions[session]['SE_a2'] = 0. 2264 2265 self.sessions[session]['b2'] = self.standardization.params.valuesdict()[f'b2_{pf(session)}'] 2266 if self.sessions[session]['slope_drift']: 2267 i = self.standardization.var_names.index(f'b2_{pf(session)}') 2268 self.sessions[session]['SE_b2'] = self.standardization.covar[i,i]**.5 2269 else: 2270 self.sessions[session]['SE_b2'] = 0. 2271 2272 self.sessions[session]['c2'] = self.standardization.params.valuesdict()[f'c2_{pf(session)}'] 2273 if self.sessions[session]['wg_drift']: 2274 i = self.standardization.var_names.index(f'c2_{pf(session)}') 2275 self.sessions[session]['SE_c2'] = self.standardization.covar[i,i]**.5 2276 else: 2277 self.sessions[session]['SE_c2'] = 0. 2278 2279 i = self.standardization.var_names.index(f'a_{pf(session)}') 2280 j = self.standardization.var_names.index(f'b_{pf(session)}') 2281 k = self.standardization.var_names.index(f'c_{pf(session)}') 2282 CM = np.zeros((6,6)) 2283 CM[:3,:3] = self.standardization.covar[[i,j,k],:][:,[i,j,k]] 2284 try: 2285 i2 = self.standardization.var_names.index(f'a2_{pf(session)}') 2286 CM[3,[0,1,2,3]] = self.standardization.covar[i2,[i,j,k,i2]] 2287 CM[[0,1,2,3],3] = self.standardization.covar[[i,j,k,i2],i2] 2288 try: 2289 j2 = self.standardization.var_names.index(f'b2_{pf(session)}') 2290 CM[3,4] = self.standardization.covar[i2,j2] 2291 CM[4,3] = self.standardization.covar[j2,i2] 2292 except ValueError: 2293 pass 2294 try: 2295 k2 = self.standardization.var_names.index(f'c2_{pf(session)}') 2296 CM[3,5] = self.standardization.covar[i2,k2] 2297 CM[5,3] = self.standardization.covar[k2,i2] 2298 except ValueError: 2299 pass 2300 except ValueError: 2301 pass 2302 try: 2303 j2 = self.standardization.var_names.index(f'b2_{pf(session)}') 2304 CM[4,[0,1,2,4]] = self.standardization.covar[j2,[i,j,k,j2]] 2305 CM[[0,1,2,4],4] = self.standardization.covar[[i,j,k,j2],j2] 2306 try: 2307 k2 = self.standardization.var_names.index(f'c2_{pf(session)}') 2308 CM[4,5] = self.standardization.covar[j2,k2] 2309 CM[5,4] = self.standardization.covar[k2,j2] 2310 except ValueError: 2311 pass 2312 except ValueError: 2313 pass 2314 try: 2315 k2 = self.standardization.var_names.index(f'c2_{pf(session)}') 2316 CM[5,[0,1,2,5]] = self.standardization.covar[k2,[i,j,k,k2]] 2317 CM[[0,1,2,5],5] = self.standardization.covar[[i,j,k,k2],k2] 2318 except ValueError: 2319 pass 2320 2321 self.sessions[session]['CM'] = CM 2322 2323 elif self.standardization_method == 'indep_sessions': 2324 pass # Not implemented yet 2325 2326 2327 @make_verbal 2328 def repeatabilities(self): 2329 ''' 2330 Compute analytical repeatabilities for δ13C_VPDB, δ18O_VSMOW, Δ4x 2331 (for all samples, for anchors, and for unknowns). 2332 ''' 2333 self.msg('Computing reproducibilities for all sessions') 2334 2335 self.repeatability['r_d13C_VPDB'] = self.compute_r('d13C_VPDB', samples = 'anchors') 2336 self.repeatability['r_d18O_VSMOW'] = self.compute_r('d18O_VSMOW', samples = 'anchors') 2337 self.repeatability[f'r_D{self._4x}a'] = self.compute_r(f'D{self._4x}', samples = 'anchors') 2338 self.repeatability[f'r_D{self._4x}u'] = self.compute_r(f'D{self._4x}', samples = 'unknowns') 2339 self.repeatability[f'r_D{self._4x}'] = self.compute_r(f'D{self._4x}', samples = 'all samples') 2340 2341 2342 @make_verbal 2343 def consolidate(self, tables = True, plots = True): 2344 ''' 2345 Collect information about samples, sessions and repeatabilities. 2346 ''' 2347 self.consolidate_samples() 2348 self.consolidate_sessions() 2349 self.repeatabilities() 2350 2351 if tables: 2352 self.summary() 2353 self.table_of_sessions() 2354 self.table_of_analyses() 2355 self.table_of_samples() 2356 2357 if plots: 2358 self.plot_sessions() 2359 2360 2361 @make_verbal 2362 def rmswd(self, 2363 samples = 'all samples', 2364 sessions = 'all sessions', 2365 ): 2366 ''' 2367 Compute the χ2, root mean squared weighted deviation 2368 (i.e. reduced χ2), and corresponding degrees of freedom of the 2369 Δ4x values for samples in `samples` and sessions in `sessions`. 2370 2371 Only used in `D4xdata.standardize()` with `method='indep_sessions'`. 2372 ''' 2373 if samples == 'all samples': 2374 mysamples = [k for k in self.samples] 2375 elif samples == 'anchors': 2376 mysamples = [k for k in self.anchors] 2377 elif samples == 'unknowns': 2378 mysamples = [k for k in self.unknowns] 2379 else: 2380 mysamples = samples 2381 2382 if sessions == 'all sessions': 2383 sessions = [k for k in self.sessions] 2384 2385 chisq, Nf = 0, 0 2386 for sample in mysamples : 2387 G = [ r for r in self if r['Sample'] == sample and r['Session'] in sessions ] 2388 if len(G) > 1 : 2389 X, sX = w_avg([r[f'D{self._4x}'] for r in G], [r[f'wD{self._4x}'] for r in G]) 2390 Nf += (len(G) - 1) 2391 chisq += np.sum([ ((r[f'D{self._4x}']-X)/r[f'wD{self._4x}'])**2 for r in G]) 2392 r = (chisq / Nf)**.5 if Nf > 0 else 0 2393 self.msg(f'RMSWD of r["D{self._4x}"] is {r:.6f} for {samples}.') 2394 return {'rmswd': r, 'chisq': chisq, 'Nf': Nf} 2395 2396 2397 @make_verbal 2398 def compute_r(self, key, samples = 'all samples', sessions = 'all sessions'): 2399 ''' 2400 Compute the repeatability of `[r[key] for r in self]` 2401 ''' 2402 # NB: it's debatable whether rD47 should be computed 2403 # with Nf = len(self)-len(self.samples) instead of 2404 # Nf = len(self) - len(self.unknwons) - 3*len(self.sessions) 2405 2406 if samples == 'all samples': 2407 mysamples = [k for k in self.samples] 2408 elif samples == 'anchors': 2409 mysamples = [k for k in self.anchors] 2410 elif samples == 'unknowns': 2411 mysamples = [k for k in self.unknowns] 2412 else: 2413 mysamples = samples 2414 2415 if sessions == 'all sessions': 2416 sessions = [k for k in self.sessions] 2417 2418 if key in ['D47', 'D48']: 2419 chisq, Nf = 0, 0 2420 for sample in mysamples : 2421 X = [ r[key] for r in self if r['Sample'] == sample and r['Session'] in sessions ] 2422 if len(X) > 1 : 2423 chisq += np.sum([ (x-self.samples[sample][key])**2 for x in X ]) 2424 if sample in self.unknowns: 2425 Nf += len(X) - 1 2426 else: 2427 Nf += len(X) 2428 if samples in ['anchors', 'all samples']: 2429 Nf -= sum([self.sessions[s]['Np'] for s in sessions]) 2430 r = (chisq / Nf)**.5 if Nf > 0 else 0 2431 2432 else: # if key not in ['D47', 'D48'] 2433 chisq, Nf = 0, 0 2434 for sample in mysamples : 2435 X = [ r[key] for r in self if r['Sample'] == sample and r['Session'] in sessions ] 2436 if len(X) > 1 : 2437 Nf += len(X) - 1 2438 chisq += np.sum([ (x-np.mean(X))**2 for x in X ]) 2439 r = (chisq / Nf)**.5 if Nf > 0 else 0 2440 2441 self.msg(f'Repeatability of r["{key}"] is {1000*r:.1f} ppm for {samples}.') 2442 return r 2443 2444 def sample_average(self, samples, weights = 'equal', normalize = True): 2445 ''' 2446 Weighted average Δ4x value of a group of samples, accounting for covariance. 2447 2448 Returns the weighed average Δ4x value and associated SE 2449 of a group of samples. Weights are equal by default. If `normalize` is 2450 true, `weights` will be rescaled so that their sum equals 1. 2451 2452 **Examples** 2453 2454 ```python 2455 self.sample_average(['X','Y'], [1, 2]) 2456 ``` 2457 2458 returns the value and SE of [Δ4x(X) + 2 Δ4x(Y)]/3, 2459 where Δ4x(X) and Δ4x(Y) are the average Δ4x 2460 values of samples X and Y, respectively. 2461 2462 ```python 2463 self.sample_average(['X','Y'], [1, -1], normalize = False) 2464 ``` 2465 2466 returns the value and SE of the difference Δ4x(X) - Δ4x(Y). 2467 ''' 2468 if weights == 'equal': 2469 weights = [1/len(samples)] * len(samples) 2470 2471 if normalize: 2472 s = sum(weights) 2473 if s: 2474 weights = [w/s for w in weights] 2475 2476 try: 2477# indices = [self.standardization.var_names.index(f'D47_{pf(sample)}') for sample in samples] 2478# C = self.standardization.covar[indices,:][:,indices] 2479 C = np.array([[self.sample_D4x_covar(x, y) for x in samples] for y in samples]) 2480 X = [self.samples[sample][f'D{self._4x}'] for sample in samples] 2481 return correlated_sum(X, C, weights) 2482 except ValueError: 2483 return (0., 0.) 2484 2485 2486 def sample_D4x_covar(self, sample1, sample2 = None): 2487 ''' 2488 Covariance between Δ4x values of samples 2489 2490 Returns the error covariance between the average Δ4x values of two 2491 samples. If if only `sample_1` is specified, or if `sample_1 == sample_2`), 2492 returns the Δ4x variance for that sample. 2493 ''' 2494 if sample2 is None: 2495 sample2 = sample1 2496 if self.standardization_method == 'pooled': 2497 i = self.standardization.var_names.index(f'D{self._4x}_{pf(sample1)}') 2498 j = self.standardization.var_names.index(f'D{self._4x}_{pf(sample2)}') 2499 return self.standardization.covar[i, j] 2500 elif self.standardization_method == 'indep_sessions': 2501 if sample1 == sample2: 2502 return self.samples[sample1][f'SE_D{self._4x}']**2 2503 else: 2504 c = 0 2505 for session in self.sessions: 2506 sdata1 = [r for r in self.sessions[session]['data'] if r['Sample'] == sample1] 2507 sdata2 = [r for r in self.sessions[session]['data'] if r['Sample'] == sample2] 2508 if sdata1 and sdata2: 2509 a = self.sessions[session]['a'] 2510 # !! TODO: CM below does not account for temporal changes in standardization parameters 2511 CM = self.sessions[session]['CM'][:3,:3] 2512 avg_D4x_1 = np.mean([r[f'D{self._4x}'] for r in sdata1]) 2513 avg_d4x_1 = np.mean([r[f'd{self._4x}'] for r in sdata1]) 2514 avg_D4x_2 = np.mean([r[f'D{self._4x}'] for r in sdata2]) 2515 avg_d4x_2 = np.mean([r[f'd{self._4x}'] for r in sdata2]) 2516 c += ( 2517 self.unknowns[sample1][f'session_D{self._4x}'][session][2] 2518 * self.unknowns[sample2][f'session_D{self._4x}'][session][2] 2519 * np.array([[avg_D4x_1, avg_d4x_1, 1]]) 2520 @ CM 2521 @ np.array([[avg_D4x_2, avg_d4x_2, 1]]).T 2522 ) / a**2 2523 return float(c) 2524 2525 def sample_D4x_correl(self, sample1, sample2 = None): 2526 ''' 2527 Correlation between Δ4x errors of samples 2528 2529 Returns the error correlation between the average Δ4x values of two samples. 2530 ''' 2531 if sample2 is None or sample2 == sample1: 2532 return 1. 2533 return ( 2534 self.sample_D4x_covar(sample1, sample2) 2535 / self.unknowns[sample1][f'SE_D{self._4x}'] 2536 / self.unknowns[sample2][f'SE_D{self._4x}'] 2537 ) 2538 2539 def plot_single_session(self, 2540 session, 2541 kw_plot_anchors = dict(ls='None', marker='x', mec=(.75, 0, 0), mew = .75, ms = 4), 2542 kw_plot_unknowns = dict(ls='None', marker='x', mec=(0, 0, .75), mew = .75, ms = 4), 2543 kw_plot_anchor_avg = dict(ls='-', marker='None', color=(.75, 0, 0), lw = .75), 2544 kw_plot_unknown_avg = dict(ls='-', marker='None', color=(0, 0, .75), lw = .75), 2545 kw_contour_error = dict(colors = [[0, 0, 0]], alpha = .5, linewidths = 0.75), 2546 xylimits = 'free', # | 'constant' 2547 x_label = None, 2548 y_label = None, 2549 error_contour_interval = 'auto', 2550 fig = 'new', 2551 ): 2552 ''' 2553 Generate plot for a single session 2554 ''' 2555 if x_label is None: 2556 x_label = f'δ$_{{{self._4x}}}$ (‰)' 2557 if y_label is None: 2558 y_label = f'Δ$_{{{self._4x}}}$ (‰)' 2559 2560 out = _SessionPlot() 2561 anchors = [a for a in self.anchors if [r for r in self.sessions[session]['data'] if r['Sample'] == a]] 2562 unknowns = [u for u in self.unknowns if [r for r in self.sessions[session]['data'] if r['Sample'] == u]] 2563 2564 if fig == 'new': 2565 out.fig = ppl.figure(figsize = (6,6)) 2566 ppl.subplots_adjust(.1,.1,.9,.9) 2567 2568 out.anchor_analyses, = ppl.plot( 2569 [r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] in self.anchors], 2570 [r[f'D{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] in self.anchors], 2571 **kw_plot_anchors) 2572 out.unknown_analyses, = ppl.plot( 2573 [r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] in self.unknowns], 2574 [r[f'D{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] in self.unknowns], 2575 **kw_plot_unknowns) 2576 out.anchor_avg = ppl.plot( 2577 np.array([ np.array([ 2578 np.min([r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] == sample]) - 1, 2579 np.max([r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] == sample]) + 1 2580 ]) for sample in anchors]).T, 2581 np.array([ np.array([0, 0]) + self.Nominal_D4x[sample] for sample in anchors]).T, 2582 **kw_plot_anchor_avg) 2583 out.unknown_avg = ppl.plot( 2584 np.array([ np.array([ 2585 np.min([r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] == sample]) - 1, 2586 np.max([r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] == sample]) + 1 2587 ]) for sample in unknowns]).T, 2588 np.array([ np.array([0, 0]) + self.unknowns[sample][f'D{self._4x}'] for sample in unknowns]).T, 2589 **kw_plot_unknown_avg) 2590 if xylimits == 'constant': 2591 x = [r[f'd{self._4x}'] for r in self] 2592 y = [r[f'D{self._4x}'] for r in self] 2593 x1, x2, y1, y2 = np.min(x), np.max(x), np.min(y), np.max(y) 2594 w, h = x2-x1, y2-y1 2595 x1 -= w/20 2596 x2 += w/20 2597 y1 -= h/20 2598 y2 += h/20 2599 ppl.axis([x1, x2, y1, y2]) 2600 elif xylimits == 'free': 2601 x1, x2, y1, y2 = ppl.axis() 2602 else: 2603 x1, x2, y1, y2 = ppl.axis(xylimits) 2604 2605 if error_contour_interval != 'none': 2606 xi, yi = np.linspace(x1, x2), np.linspace(y1, y2) 2607 XI,YI = np.meshgrid(xi, yi) 2608 SI = np.array([[self.standardization_error(session, x, y) for x in xi] for y in yi]) 2609 if error_contour_interval == 'auto': 2610 rng = np.max(SI) - np.min(SI) 2611 if rng <= 0.01: 2612 cinterval = 0.001 2613 elif rng <= 0.03: 2614 cinterval = 0.004 2615 elif rng <= 0.1: 2616 cinterval = 0.01 2617 elif rng <= 0.3: 2618 cinterval = 0.03 2619 elif rng <= 1.: 2620 cinterval = 0.1 2621 else: 2622 cinterval = 0.5 2623 else: 2624 cinterval = error_contour_interval 2625 2626 cval = np.arange(np.ceil(SI.min() / .001) * .001, np.ceil(SI.max() / .001 + 1) * .001, cinterval) 2627 out.contour = ppl.contour(XI, YI, SI, cval, **kw_contour_error) 2628 out.clabel = ppl.clabel(out.contour) 2629 2630 ppl.xlabel(x_label) 2631 ppl.ylabel(y_label) 2632 ppl.title(session, weight = 'bold') 2633 ppl.grid(alpha = .2) 2634 out.ax = ppl.gca() 2635 2636 return out 2637 2638 def plot_residuals( 2639 self, 2640 hist = False, 2641 binwidth = 2/3, 2642 dir = 'output', 2643 filename = None, 2644 highlight = [], 2645 colors = None, 2646 figsize = None, 2647 ): 2648 ''' 2649 Plot residuals of each analysis as a function of time (actually, as a function of 2650 the order of analyses in the `D4xdata` object) 2651 2652 + `hist`: whether to add a histogram of residuals 2653 + `histbins`: specify bin edges for the histogram 2654 + `dir`: the directory in which to save the plot 2655 + `highlight`: a list of samples to highlight 2656 + `colors`: a dict of `{<sample>: <color>}` for all samples 2657 + `figsize`: (width, height) of figure 2658 ''' 2659 # Layout 2660 fig = ppl.figure(figsize = (8,4) if figsize is None else figsize) 2661 if hist: 2662 ppl.subplots_adjust(left = .08, bottom = .05, right = .98, top = .8, wspace = -0.72) 2663 ax1, ax2 = ppl.subplot(121), ppl.subplot(1,15,15) 2664 else: 2665 ppl.subplots_adjust(.08,.05,.78,.8) 2666 ax1 = ppl.subplot(111) 2667 2668 # Colors 2669 N = len(self.anchors) 2670 if colors is None: 2671 if len(highlight) > 0: 2672 Nh = len(highlight) 2673 if Nh == 1: 2674 colors = {highlight[0]: (0,0,0)} 2675 elif Nh == 3: 2676 colors = {a: c for a,c in zip(highlight, [(0,0,1), (1,0,0), (0,2/3,0)])} 2677 elif Nh == 4: 2678 colors = {a: c for a,c in zip(highlight, [(0,0,1), (1,0,0), (0,2/3,0), (.75,0,.75)])} 2679 else: 2680 colors = {a: hls_to_rgb(k/Nh, .4, 1) for k,a in enumerate(highlight)} 2681 else: 2682 if N == 3: 2683 colors = {a: c for a,c in zip(self.anchors, [(0,0,1), (1,0,0), (0,2/3,0)])} 2684 elif N == 4: 2685 colors = {a: c for a,c in zip(self.anchors, [(0,0,1), (1,0,0), (0,2/3,0), (.75,0,.75)])} 2686 else: 2687 colors = {a: hls_to_rgb(k/N, .4, 1) for k,a in enumerate(self.anchors)} 2688 2689 ppl.sca(ax1) 2690 2691 ppl.axhline(0, color = 'k', alpha = .25, lw = 0.75) 2692 2693 session = self[0]['Session'] 2694 x1 = 0 2695# ymax = np.max([1e3 * (r['D47'] - self.samples[r['Sample']]['D47']) for r in self]) 2696 x_sessions = {} 2697 one_or_more_singlets = False 2698 one_or_more_multiplets = False 2699 multiplets = set() 2700 for k,r in enumerate(self): 2701 if r['Session'] != session: 2702 x2 = k-1 2703 x_sessions[session] = (x1+x2)/2 2704 ppl.axvline(k - 0.5, color = 'k', lw = .5) 2705 session = r['Session'] 2706 x1 = k 2707 singlet = len(self.samples[r['Sample']]['data']) == 1 2708 if not singlet: 2709 multiplets.add(r['Sample']) 2710 if r['Sample'] in self.unknowns: 2711 if singlet: 2712 one_or_more_singlets = True 2713 else: 2714 one_or_more_multiplets = True 2715 kw = dict( 2716 marker = 'x' if singlet else '+', 2717 ms = 4 if singlet else 5, 2718 ls = 'None', 2719 mec = colors[r['Sample']] if r['Sample'] in colors else (0,0,0), 2720 mew = 1, 2721 alpha = 0.2 if singlet else 1, 2722 ) 2723 if highlight and r['Sample'] not in highlight: 2724 kw['alpha'] = 0.2 2725 ppl.plot(k, 1e3 * (r['D47'] - self.samples[r['Sample']]['D47']), **kw) 2726 x2 = k 2727 x_sessions[session] = (x1+x2)/2 2728 2729 ppl.axhspan(-self.repeatability['r_D47']*1000, self.repeatability['r_D47']*1000, color = 'k', alpha = .05, lw = 1) 2730 ppl.axhspan(-self.repeatability['r_D47']*1000*self.t95, self.repeatability['r_D47']*1000*self.t95, color = 'k', alpha = .05, lw = 1) 2731 if not hist: 2732 ppl.text(len(self), self.repeatability['r_D47']*1000, f" SD = {self.repeatability['r_D47']*1000:.1f} ppm", size = 9, alpha = 1, va = 'center') 2733 ppl.text(len(self), self.repeatability['r_D47']*1000*self.t95, f" 95% CL = ± {self.repeatability['r_D47']*1000*self.t95:.1f} ppm", size = 9, alpha = 1, va = 'center') 2734 2735 xmin, xmax, ymin, ymax = ppl.axis() 2736 for s in x_sessions: 2737 ppl.text( 2738 x_sessions[s], 2739 ymax +1, 2740 s, 2741 va = 'bottom', 2742 **( 2743 dict(ha = 'center') 2744 if len(self.sessions[s]['data']) > (0.15 * len(self)) 2745 else dict(ha = 'left', rotation = 45) 2746 ) 2747 ) 2748 2749 if hist: 2750 ppl.sca(ax2) 2751 2752 for s in colors: 2753 kw['marker'] = '+' 2754 kw['ms'] = 5 2755 kw['mec'] = colors[s] 2756 kw['label'] = s 2757 kw['alpha'] = 1 2758 ppl.plot([], [], **kw) 2759 2760 kw['mec'] = (0,0,0) 2761 2762 if one_or_more_singlets: 2763 kw['marker'] = 'x' 2764 kw['ms'] = 4 2765 kw['alpha'] = .2 2766 kw['label'] = 'other (N$\\,$=$\\,$1)' if one_or_more_multiplets else 'other' 2767 ppl.plot([], [], **kw) 2768 2769 if one_or_more_multiplets: 2770 kw['marker'] = '+' 2771 kw['ms'] = 4 2772 kw['alpha'] = 1 2773 kw['label'] = 'other (N$\\,$>$\\,$1)' if one_or_more_singlets else 'other' 2774 ppl.plot([], [], **kw) 2775 2776 if hist: 2777 leg = ppl.legend(loc = 'upper right', bbox_to_anchor = (1, 1), bbox_transform=fig.transFigure, borderaxespad = 1.5, fontsize = 9) 2778 else: 2779 leg = ppl.legend(loc = 'lower right', bbox_to_anchor = (1, 0), bbox_transform=fig.transFigure, borderaxespad = 1.5) 2780 leg.set_zorder(-1000) 2781 2782 ppl.sca(ax1) 2783 2784 ppl.ylabel('Δ$_{47}$ residuals (ppm)') 2785 ppl.xticks([]) 2786 ppl.axis([-1, len(self), None, None]) 2787 2788 if hist: 2789 ppl.sca(ax2) 2790 X = [1e3 * (r['D47'] - self.samples[r['Sample']]['D47']) for r in self if r['Sample'] in multiplets] 2791 ppl.hist( 2792 X, 2793 orientation = 'horizontal', 2794 histtype = 'stepfilled', 2795 ec = [.4]*3, 2796 fc = [.25]*3, 2797 alpha = .25, 2798 bins = np.linspace(-9e3*self.repeatability['r_D47'], 9e3*self.repeatability['r_D47'], int(18/binwidth+1)), 2799 ) 2800 ppl.axis([None, None, ymin, ymax]) 2801 ppl.text(0, 0, 2802 f" SD = {self.repeatability['r_D47']*1000:.1f} ppm\n 95% CL = ± {self.repeatability['r_D47']*1000*self.t95:.1f} ppm", 2803 size = 8, 2804 alpha = 1, 2805 va = 'center', 2806 ha = 'left', 2807 ) 2808 2809 ppl.xticks([]) 2810 ppl.yticks([]) 2811# ax2.spines['left'].set_visible(False) 2812 ax2.spines['right'].set_visible(False) 2813 ax2.spines['top'].set_visible(False) 2814 ax2.spines['bottom'].set_visible(False) 2815 2816 2817 if not os.path.exists(dir): 2818 os.makedirs(dir) 2819 if filename is None: 2820 return fig 2821 elif filename == '': 2822 filename = f'D{self._4x}_residuals.pdf' 2823 ppl.savefig(f'{dir}/{filename}') 2824 ppl.close(fig) 2825 2826 2827 def simulate(self, *args, **kwargs): 2828 ''' 2829 Legacy function with warning message pointing to `virtual_data()` 2830 ''' 2831 raise DeprecationWarning('D4xdata.simulate is deprecated and has been replaced by virtual_data()') 2832 2833 def plot_distribution_of_analyses( 2834 self, 2835 dir = 'output', 2836 filename = None, 2837 vs_time = False, 2838 figsize = (6,4), 2839 subplots_adjust = (0.02, 0.13, 0.85, 0.8), 2840 output = None, 2841 ): 2842 ''' 2843 Plot temporal distribution of all analyses in the data set. 2844 2845 **Parameters** 2846 2847 + `vs_time`: if `True`, plot as a function of `TimeTag` rather than sequentially. 2848 ''' 2849 2850 asamples = [s for s in self.anchors] 2851 usamples = [s for s in self.unknowns] 2852 if output is None or output == 'fig': 2853 fig = ppl.figure(figsize = figsize) 2854 ppl.subplots_adjust(*subplots_adjust) 2855 Xmin = min([r['TimeTag'] if vs_time else j for j,r in enumerate(self)]) 2856 Xmax = max([r['TimeTag'] if vs_time else j for j,r in enumerate(self)]) 2857 Xmax += (Xmax-Xmin)/40 2858 Xmin -= (Xmax-Xmin)/41 2859 for k, s in enumerate(asamples + usamples): 2860 if vs_time: 2861 X = [r['TimeTag'] for r in self if r['Sample'] == s] 2862 else: 2863 X = [x for x,r in enumerate(self) if r['Sample'] == s] 2864 Y = [-k for x in X] 2865 ppl.plot(X, Y, 'o', mec = None, mew = 0, mfc = 'b' if s in usamples else 'r', ms = 3, alpha = .75) 2866 ppl.axhline(-k, color = 'b' if s in usamples else 'r', lw = .5, alpha = .25) 2867 ppl.text(Xmax, -k, f' {s}', va = 'center', ha = 'left', size = 7, color = 'b' if s in usamples else 'r') 2868 ppl.axis([Xmin, Xmax, -k-1, 1]) 2869 ppl.xlabel('\ntime') 2870 ppl.gca().annotate('', 2871 xy = (0.6, -0.02), 2872 xycoords = 'axes fraction', 2873 xytext = (.4, -0.02), 2874 arrowprops = dict(arrowstyle = "->", color = 'k'), 2875 ) 2876 2877 2878 x2 = -1 2879 for session in self.sessions: 2880 x1 = min([r['TimeTag'] if vs_time else j for j,r in enumerate(self) if r['Session'] == session]) 2881 if vs_time: 2882 ppl.axvline(x1, color = 'k', lw = .75) 2883 if x2 > -1: 2884 if not vs_time: 2885 ppl.axvline((x1+x2)/2, color = 'k', lw = .75, alpha = .5) 2886 x2 = max([r['TimeTag'] if vs_time else j for j,r in enumerate(self) if r['Session'] == session]) 2887# from xlrd import xldate_as_datetime 2888# print(session, xldate_as_datetime(x1, 0), xldate_as_datetime(x2, 0)) 2889 if vs_time: 2890 ppl.axvline(x2, color = 'k', lw = .75) 2891 ppl.axvspan(x1,x2,color = 'k', zorder = -100, alpha = .15) 2892 ppl.text((x1+x2)/2, 1, f' {session}', ha = 'left', va = 'bottom', rotation = 45, size = 8) 2893 2894 ppl.xticks([]) 2895 ppl.yticks([]) 2896 2897 if output is None: 2898 if not os.path.exists(dir): 2899 os.makedirs(dir) 2900 if filename == None: 2901 filename = f'D{self._4x}_distribution_of_analyses.pdf' 2902 ppl.savefig(f'{dir}/{filename}') 2903 ppl.close(fig) 2904 elif output == 'ax': 2905 return ppl.gca() 2906 elif output == 'fig': 2907 return fig 2908 2909 2910class D47data(D4xdata): 2911 ''' 2912 Store and process data for a large set of Δ47 analyses, 2913 usually comprising more than one analytical session. 2914 ''' 2915 2916 Nominal_D4x = { 2917 'ETH-1': 0.2052, 2918 'ETH-2': 0.2085, 2919 'ETH-3': 0.6132, 2920 'ETH-4': 0.4511, 2921 'IAEA-C1': 0.3018, 2922 'IAEA-C2': 0.6409, 2923 'MERCK': 0.5135, 2924 } # I-CDES (Bernasconi et al., 2021) 2925 ''' 2926 Nominal Δ47 values assigned to the Δ47 anchor samples, used by 2927 `D47data.standardize()` to normalize unknown samples to an absolute Δ47 2928 reference frame. 2929 2930 By default equal to (after [Bernasconi et al. (2021)](https://doi.org/10.1029/2020GC009588)): 2931 ```py 2932 { 2933 'ETH-1' : 0.2052, 2934 'ETH-2' : 0.2085, 2935 'ETH-3' : 0.6132, 2936 'ETH-4' : 0.4511, 2937 'IAEA-C1' : 0.3018, 2938 'IAEA-C2' : 0.6409, 2939 'MERCK' : 0.5135, 2940 } 2941 ``` 2942 ''' 2943 2944 2945 @property 2946 def Nominal_D47(self): 2947 return self.Nominal_D4x 2948 2949 2950 @Nominal_D47.setter 2951 def Nominal_D47(self, new): 2952 self.Nominal_D4x = dict(**new) 2953 self.refresh() 2954 2955 2956 def __init__(self, l = [], **kwargs): 2957 ''' 2958 **Parameters:** same as `D4xdata.__init__()` 2959 ''' 2960 D4xdata.__init__(self, l = l, mass = '47', **kwargs) 2961 2962 2963 def D47fromTeq(self, fCo2eqD47 = 'petersen', priority = 'new'): 2964 ''' 2965 Find all samples for which `Teq` is specified, compute equilibrium Δ47 2966 value for that temperature, and add treat these samples as additional anchors. 2967 2968 **Parameters** 2969 2970 + `fCo2eqD47`: Which CO2 equilibrium law to use 2971 (`petersen`: [Petersen et al. (2019)](https://doi.org/10.1029/2018GC008127); 2972 `wang`: [Wang et al. (2019)](https://doi.org/10.1016/j.gca.2004.05.039)). 2973 + `priority`: if `replace`: forget old anchors and only use the new ones; 2974 if `new`: keep pre-existing anchors but update them in case of conflict 2975 between old and new Δ47 values; 2976 if `old`: keep pre-existing anchors but preserve their original Δ47 2977 values in case of conflict. 2978 ''' 2979 f = { 2980 'petersen': fCO2eqD47_Petersen, 2981 'wang': fCO2eqD47_Wang, 2982 }[fCo2eqD47] 2983 foo = {} 2984 for r in self: 2985 if 'Teq' in r: 2986 if r['Sample'] in foo: 2987 assert foo[r['Sample']] == f(r['Teq']), f'Different values of `Teq` provided for sample `{r["Sample"]}`.' 2988 else: 2989 foo[r['Sample']] = f(r['Teq']) 2990 else: 2991 assert r['Sample'] not in foo, f'`Teq` is inconsistently specified for sample `{r["Sample"]}`.' 2992 2993 if priority == 'replace': 2994 self.Nominal_D47 = {} 2995 for s in foo: 2996 if priority != 'old' or s not in self.Nominal_D47: 2997 self.Nominal_D47[s] = foo[s] 2998 2999 3000 3001 3002class D48data(D4xdata): 3003 ''' 3004 Store and process data for a large set of Δ48 analyses, 3005 usually comprising more than one analytical session. 3006 ''' 3007 3008 Nominal_D4x = { 3009 'ETH-1': 0.138, 3010 'ETH-2': 0.138, 3011 'ETH-3': 0.270, 3012 'ETH-4': 0.223, 3013 'GU-1': -0.419, 3014 } # (Fiebig et al., 2019, 2021) 3015 ''' 3016 Nominal Δ48 values assigned to the Δ48 anchor samples, used by 3017 `D48data.standardize()` to normalize unknown samples to an absolute Δ48 3018 reference frame. 3019 3020 By default equal to (after [Fiebig et al. (2019)](https://doi.org/10.1016/j.chemgeo.2019.05.019), 3021 Fiebig et al. (in press)): 3022 3023 ```py 3024 { 3025 'ETH-1' : 0.138, 3026 'ETH-2' : 0.138, 3027 'ETH-3' : 0.270, 3028 'ETH-4' : 0.223, 3029 'GU-1' : -0.419, 3030 } 3031 ``` 3032 ''' 3033 3034 3035 @property 3036 def Nominal_D48(self): 3037 return self.Nominal_D4x 3038 3039 3040 @Nominal_D48.setter 3041 def Nominal_D48(self, new): 3042 self.Nominal_D4x = dict(**new) 3043 self.refresh() 3044 3045 3046 def __init__(self, l = [], **kwargs): 3047 ''' 3048 **Parameters:** same as `D4xdata.__init__()` 3049 ''' 3050 D4xdata.__init__(self, l = l, mass = '48', **kwargs) 3051 3052 3053class _SessionPlot(): 3054 ''' 3055 Simple placeholder class 3056 ''' 3057 def __init__(self): 3058 pass ======= 14 15## API Documentation 16''' 17 18__docformat__ = "restructuredtext" 19__author__ = 'Mathieu Daëron' 20__contact__ = 'daeron@lsce.ipsl.fr' 21__copyright__ = 'Copyright (c) 2023 Mathieu Daëron' 22__license__ = 'Modified BSD License - https://opensource.org/licenses/BSD-3-Clause' 23__date__ = '2023-05-11' 24__version__ = '2.0.4' 25 26import os 27import numpy as np 28from statistics import stdev 29from scipy.stats import t as tstudent 30from scipy.stats import levene 31from scipy.interpolate import interp1d 32from numpy import linalg 33from lmfit import Minimizer, Parameters, report_fit 34from matplotlib import pyplot as ppl 35from datetime import datetime as dt 36from functools import wraps 37from colorsys import hls_to_rgb 38from matplotlib import rcParams 39 40rcParams['font.family'] = 'sans-serif' 41rcParams['font.sans-serif'] = 'Helvetica' 42rcParams['font.size'] = 10 43rcParams['mathtext.fontset'] = 'custom' 44rcParams['mathtext.rm'] = 'sans' 45rcParams['mathtext.bf'] = 'sans:bold' 46rcParams['mathtext.it'] = 'sans:italic' 47rcParams['mathtext.cal'] = 'sans:italic' 48rcParams['mathtext.default'] = 'rm' 49rcParams['xtick.major.size'] = 4 50rcParams['xtick.major.width'] = 1 51rcParams['ytick.major.size'] = 4 52rcParams['ytick.major.width'] = 1 53rcParams['axes.grid'] = False 54rcParams['axes.linewidth'] = 1 55rcParams['grid.linewidth'] = .75 56rcParams['grid.linestyle'] = '-' 57rcParams['grid.alpha'] = .15 58rcParams['savefig.dpi'] = 150 59 60Petersen_etal_CO2eqD47 = np.array([[-12, 1.147113572], [-11, 1.139961218], [-10, 1.132872856], [-9, 1.125847677], [-8, 1.118884889], [-7, 1.111983708], [-6, 1.105143366], [-5, 1.098363105], [-4, 1.091642182], [-3, 1.084979862], [-2, 1.078375423], [-1, 1.071828156], [0, 1.065337360], [1, 1.058902349], [2, 1.052522443], [3, 1.046196976], [4, 1.039925291], [5, 1.033706741], [6, 1.027540690], [7, 1.021426510], [8, 1.015363585], [9, 1.009351306], [10, 1.003389075], [11, 0.997476303], [12, 0.991612409], [13, 0.985796821], [14, 0.980028975], [15, 0.974308318], [16, 0.968634304], [17, 0.963006392], [18, 0.957424055], [19, 0.951886769], [20, 0.946394020], [21, 0.940945302], [22, 0.935540114], [23, 0.930177964], [24, 0.924858369], [25, 0.919580851], [26, 0.914344938], [27, 0.909150167], [28, 0.903996080], [29, 0.898882228], [30, 0.893808167], [31, 0.888773459], [32, 0.883777672], [33, 0.878820382], [34, 0.873901170], [35, 0.869019623], [36, 0.864175334], [37, 0.859367901], [38, 0.854596929], [39, 0.849862028], [40, 0.845162813], [41, 0.840498905], [42, 0.835869931], [43, 0.831275522], [44, 0.826715314], [45, 0.822188950], [46, 0.817696075], [47, 0.813236341], [48, 0.808809404], [49, 0.804414926], [50, 0.800052572], [51, 0.795722012], [52, 0.791422922], [53, 0.787154979], [54, 0.782917869], [55, 0.778711277], [56, 0.774534898], [57, 0.770388426], [58, 0.766271562], [59, 0.762184010], [60, 0.758125479], [61, 0.754095680], [62, 0.750094329], [63, 0.746121147], [64, 0.742175856], [65, 0.738258184], [66, 0.734367860], [67, 0.730504620], [68, 0.726668201], [69, 0.722858343], [70, 0.719074792], [71, 0.715317295], [72, 0.711585602], [73, 0.707879469], [74, 0.704198652], [75, 0.700542912], [76, 0.696912012], [77, 0.693305719], [78, 0.689723802], [79, 0.686166034], [80, 0.682632189], [81, 0.679122047], [82, 0.675635387], [83, 0.672171994], [84, 0.668731654], [85, 0.665314156], [86, 0.661919291], [87, 0.658546854], [88, 0.655196641], [89, 0.651868451], [90, 0.648562087], [91, 0.645277352], [92, 0.642014054], [93, 0.638771999], [94, 0.635551001], [95, 0.632350872], [96, 0.629171428], [97, 0.626012487], [98, 0.622873870], [99, 0.619755397], [100, 0.616656895], [102, 0.610519107], [104, 0.604459143], [106, 0.598475670], [108, 0.592567388], [110, 0.586733026], [112, 0.580971342], [114, 0.575281125], [116, 0.569661187], [118, 0.564110371], [120, 0.558627545], [122, 0.553211600], [124, 0.547861454], [126, 0.542576048], [128, 0.537354347], [130, 0.532195337], [132, 0.527098028], [134, 0.522061450], [136, 0.517084654], [138, 0.512166711], [140, 0.507306712], [142, 0.502503768], [144, 0.497757006], [146, 0.493065573], [148, 0.488428634], [150, 0.483845370], [152, 0.479314980], [154, 0.474836677], [156, 0.470409692], [158, 0.466033271], [160, 0.461706674], [162, 0.457429176], [164, 0.453200067], [166, 0.449018650], [168, 0.444884242], [170, 0.440796174], [172, 0.436753787], [174, 0.432756438], [176, 0.428803494], [178, 0.424894334], [180, 0.421028350], [182, 0.417204944], [184, 0.413423530], [186, 0.409683531], [188, 0.405984383], [190, 0.402325531], [192, 0.398706429], [194, 0.395126543], [196, 0.391585347], [198, 0.388082324], [200, 0.384616967], [202, 0.381188778], [204, 0.377797268], [206, 0.374441954], [208, 0.371122364], [210, 0.367838033], [212, 0.364588505], [214, 0.361373329], [216, 0.358192065], [218, 0.355044277], [220, 0.351929540], [222, 0.348847432], [224, 0.345797540], [226, 0.342779460], [228, 0.339792789], [230, 0.336837136], [232, 0.333912113], [234, 0.331017339], [236, 0.328152439], [238, 0.325317046], [240, 0.322510795], [242, 0.319733329], [244, 0.316984297], [246, 0.314263352], [248, 0.311570153], [250, 0.308904364], [252, 0.306265654], [254, 0.303653699], [256, 0.301068176], [258, 0.298508771], [260, 0.295975171], [262, 0.293467070], [264, 0.290984167], [266, 0.288526163], [268, 0.286092765], [270, 0.283683684], [272, 0.281298636], [274, 0.278937339], [276, 0.276599517], [278, 0.274284898], [280, 0.271993211], [282, 0.269724193], [284, 0.267477582], [286, 0.265253121], [288, 0.263050554], [290, 0.260869633], [292, 0.258710110], [294, 0.256571741], [296, 0.254454286], [298, 0.252357508], [300, 0.250281174], [302, 0.248225053], [304, 0.246188917], [306, 0.244172542], [308, 0.242175707], [310, 0.240198194], [312, 0.238239786], [314, 0.236300272], [316, 0.234379441], [318, 0.232477087], [320, 0.230593005], [322, 0.228726993], [324, 0.226878853], [326, 0.225048388], [328, 0.223235405], [330, 0.221439711], [332, 0.219661118], [334, 0.217899439], [336, 0.216154491], [338, 0.214426091], [340, 0.212714060], [342, 0.211018220], [344, 0.209338398], [346, 0.207674420], [348, 0.206026115], [350, 0.204393315], [355, 0.200378063], [360, 0.196456139], [365, 0.192625077], [370, 0.188882487], [375, 0.185226048], [380, 0.181653511], [385, 0.178162694], [390, 0.174751478], [395, 0.171417807], [400, 0.168159686], [405, 0.164975177], [410, 0.161862398], [415, 0.158819521], [420, 0.155844772], [425, 0.152936426], [430, 0.150092806], [435, 0.147312286], [440, 0.144593281], [445, 0.141934254], [450, 0.139333710], [455, 0.136790195], [460, 0.134302294], [465, 0.131868634], [470, 0.129487876], [475, 0.127158722], [480, 0.124879906], [485, 0.122650197], [490, 0.120468398], [495, 0.118333345], [500, 0.116243903], [505, 0.114198970], [510, 0.112197471], [515, 0.110238362], [520, 0.108320625], [525, 0.106443271], [530, 0.104605335], [535, 0.102805877], [540, 0.101043985], [545, 0.099318768], [550, 0.097629359], [555, 0.095974915], [560, 0.094354612], [565, 0.092767650], [570, 0.091213248], [575, 0.089690648], [580, 0.088199108], [585, 0.086737906], [590, 0.085306341], [595, 0.083903726], [600, 0.082529395], [605, 0.081182697], [610, 0.079862998], [615, 0.078569680], [620, 0.077302141], [625, 0.076059794], [630, 0.074842066], [635, 0.073648400], [640, 0.072478251], [645, 0.071331090], [650, 0.070206399], [655, 0.069103674], [660, 0.068022424], [665, 0.066962168], [670, 0.065922439], [675, 0.064902780], [680, 0.063902748], [685, 0.062921909], [690, 0.061959837], [695, 0.061016122], [700, 0.060090360], [705, 0.059182157], [710, 0.058291131], [715, 0.057416907], [720, 0.056559120], [725, 0.055717414], [730, 0.054891440], [735, 0.054080860], [740, 0.053285343], [745, 0.052504565], [750, 0.051738210], [755, 0.050985971], [760, 0.050247546], [765, 0.049522643], [770, 0.048810974], [775, 0.048112260], [780, 0.047426227], [785, 0.046752609], [790, 0.046091145], [795, 0.045441581], [800, 0.044803668], [805, 0.044177164], [810, 0.043561831], [815, 0.042957438], [820, 0.042363759], [825, 0.041780573], [830, 0.041207664], [835, 0.040644822], [840, 0.040091839], [845, 0.039548516], [850, 0.039014654], [855, 0.038490063], [860, 0.037974554], [865, 0.037467944], [870, 0.036970054], [875, 0.036480707], [880, 0.035999734], [885, 0.035526965], [890, 0.035062238], [895, 0.034605393], [900, 0.034156272], [905, 0.033714724], [910, 0.033280598], [915, 0.032853749], [920, 0.032434032], [925, 0.032021309], [930, 0.031615443], [935, 0.031216300], [940, 0.030823749], [945, 0.030437663], [950, 0.030057915], [955, 0.029684385], [960, 0.029316951], [965, 0.028955498], [970, 0.028599910], [975, 0.028250075], [980, 0.027905884], [985, 0.027567229], [990, 0.027234006], [995, 0.026906112], [1000, 0.026583445], [1005, 0.026265908], [1010, 0.025953405], [1015, 0.025645841], [1020, 0.025343124], [1025, 0.025045163], [1030, 0.024751871], [1035, 0.024463160], [1040, 0.024178947], [1045, 0.023899147], [1050, 0.023623680], [1055, 0.023352467], [1060, 0.023085429], [1065, 0.022822491], [1070, 0.022563577], [1075, 0.022308615], [1080, 0.022057533], [1085, 0.021810260], [1090, 0.021566729], [1095, 0.021326872], [1100, 0.021090622]]) 61_fCO2eqD47_Petersen = interp1d(Petersen_etal_CO2eqD47[:,0], Petersen_etal_CO2eqD47[:,1]) 62def fCO2eqD47_Petersen(T): 63 ''' 64 CO2 equilibrium Δ47 value as a function of T (in degrees C) 65 according to [Petersen et al. (2019)](https://doi.org/10.1029/2018GC008127). 66 67 ''' 68 return float(_fCO2eqD47_Petersen(T)) 69 70 71Wang_etal_CO2eqD47 = np.array([[-83., 1.8954], [-73., 1.7530], [-63., 1.6261], [-53., 1.5126], [-43., 1.4104], [-33., 1.3182], [-23., 1.2345], [-13., 1.1584], [-3., 1.0888], [7., 1.0251], [17., 0.9665], [27., 0.9125], [37., 0.8626], [47., 0.8164], [57., 0.7734], [67., 0.7334], [87., 0.6612], [97., 0.6286], [107., 0.5980], [117., 0.5693], [127., 0.5423], [137., 0.5169], [147., 0.4930], [157., 0.4704], [167., 0.4491], [177., 0.4289], [187., 0.4098], [197., 0.3918], [207., 0.3747], [217., 0.3585], [227., 0.3431], [237., 0.3285], [247., 0.3147], [257., 0.3015], [267., 0.2890], [277., 0.2771], [287., 0.2657], [297., 0.2550], [307., 0.2447], [317., 0.2349], [327., 0.2256], [337., 0.2167], [347., 0.2083], [357., 0.2002], [367., 0.1925], [377., 0.1851], [387., 0.1781], [397., 0.1714], [407., 0.1650], [417., 0.1589], [427., 0.1530], [437., 0.1474], [447., 0.1421], [457., 0.1370], [467., 0.1321], [477., 0.1274], [487., 0.1229], [497., 0.1186], [507., 0.1145], [517., 0.1105], [527., 0.1068], [537., 0.1031], [547., 0.0997], [557., 0.0963], [567., 0.0931], [577., 0.0901], [587., 0.0871], [597., 0.0843], [607., 0.0816], [617., 0.0790], [627., 0.0765], [637., 0.0741], [647., 0.0718], [657., 0.0695], [667., 0.0674], [677., 0.0654], [687., 0.0634], [697., 0.0615], [707., 0.0597], [717., 0.0579], [727., 0.0562], [737., 0.0546], [747., 0.0530], [757., 0.0515], [767., 0.0500], [777., 0.0486], [787., 0.0472], [797., 0.0459], [807., 0.0447], [817., 0.0435], [827., 0.0423], [837., 0.0411], [847., 0.0400], [857., 0.0390], [867., 0.0380], [877., 0.0370], [887., 0.0360], [897., 0.0351], [907., 0.0342], [917., 0.0333], [927., 0.0325], [937., 0.0317], [947., 0.0309], [957., 0.0302], [967., 0.0294], [977., 0.0287], [987., 0.0281], [997., 0.0274], [1007., 0.0268], [1017., 0.0261], [1027., 0.0255], [1037., 0.0249], [1047., 0.0244], [1057., 0.0238], [1067., 0.0233], [1077., 0.0228], [1087., 0.0223], [1097., 0.0218]]) 72_fCO2eqD47_Wang = interp1d(Wang_etal_CO2eqD47[:,0] - 0.15, Wang_etal_CO2eqD47[:,1]) 73def fCO2eqD47_Wang(T): 74 ''' 75 CO2 equilibrium Δ47 value as a function of `T` (in degrees C) 76 according to [Wang et al. (2004)](https://doi.org/10.1016/j.gca.2004.05.039) 77 (supplementary data of [Dennis et al., 2011](https://doi.org/10.1016/j.gca.2011.09.025)). 78 ''' 79 return float(_fCO2eqD47_Wang(T)) 80 81 82def correlated_sum(X, C, w = None): 83 ''' 84 Compute covariance-aware linear combinations 85 86 **Parameters** 87 88 + `X`: list or 1-D array of values to sum 89 + `C`: covariance matrix for the elements of `X` 90 + `w`: list or 1-D array of weights to apply to the elements of `X` 91 (all equal to 1 by default) 92 93 Return the sum (and its SE) of the elements of `X`, with optional weights equal 94 to the elements of `w`, accounting for covariances between the elements of `X`. 95 ''' 96 if w is None: 97 w = [1 for x in X] 98 return np.dot(w,X), (np.dot(w,np.dot(C,w)))**.5 99 100 101def make_csv(x, hsep = ',', vsep = '\n'): 102 ''' 103 Formats a list of lists of strings as a CSV 104 105 **Parameters** 106 107 + `x`: the list of lists of strings to format 108 + `hsep`: the field separator (`,` by default) 109 + `vsep`: the line-ending convention to use (`\\n` by default) 110 111 **Example** 112 113 ```py 114 print(make_csv([['a', 'b', 'c'], ['d', 'e', 'f']])) 115 ``` 116 117 outputs: 118 119 ```py 120 a,b,c 121 d,e,f 122 ``` 123 ''' 124 return vsep.join([hsep.join(l) for l in x]) 125 126 127def pf(txt): 128 ''' 129 Modify string `txt` to follow `lmfit.Parameter()` naming rules. 130 ''' 131 return txt.replace('-','_').replace('.','_').replace(' ','_') 132 133 134def smart_type(x): 135 ''' 136 Tries to convert string `x` to a float if it includes a decimal point, or 137 to an integer if it does not. If both attempts fail, return the original 138 string unchanged. 139 ''' 140 try: 141 y = float(x) 142 except ValueError: 143 return x 144 if '.' not in x: 145 return int(y) 146 return y 147 148 149def pretty_table(x, header = 1, hsep = ' ', vsep = '–', align = '<'): 150 ''' 151 Reads a list of lists of strings and outputs an ascii table 152 153 **Parameters** 154 155 + `x`: a list of lists of strings 156 + `header`: the number of lines to treat as header lines 157 + `hsep`: the horizontal separator between columns 158 + `vsep`: the character to use as vertical separator 159 + `align`: string of left (`<`) or right (`>`) alignment characters. 160 161 **Example** 162 163 ```py 164 x = [['A', 'B', 'C'], ['1', '1.9999', 'foo'], ['10', 'x', 'bar']] 165 print(pretty_table(x)) 166 ``` 167 yields: 168 ``` 169 -- ------ --- 170 A B C 171 -- ------ --- 172 1 1.9999 foo 173 10 x bar 174 -- ------ --- 175 ``` 176 177 ''' 178 txt = [] 179 widths = [np.max([len(e) for e in c]) for c in zip(*x)] 180 181 if len(widths) > len(align): 182 align += '>' * (len(widths)-len(align)) 183 sepline = hsep.join([vsep*w for w in widths]) 184 txt += [sepline] 185 for k,l in enumerate(x): 186 if k and k == header: 187 txt += [sepline] 188 txt += [hsep.join([f'{e:{a}{w}}' for e, w, a in zip(l, widths, align)])] 189 txt += [sepline] 190 txt += [''] 191 return '\n'.join(txt) 192 193 194def transpose_table(x): 195 ''' 196 Transpose a list if lists 197 198 **Parameters** 199 200 + `x`: a list of lists 201 202 **Example** 203 204 ```py 205 x = [[1, 2], [3, 4]] 206 print(transpose_table(x)) # yields: [[1, 3], [2, 4]] 207 ``` 208 ''' 209 return [[e for e in c] for c in zip(*x)] 210 211 212def w_avg(X, sX) : 213 ''' 214 Compute variance-weighted average 215 216 Returns the value and SE of the weighted average of the elements of `X`, 217 with relative weights equal to their inverse variances (`1/sX**2`). 218 219 **Parameters** 220 221 + `X`: array-like of elements to average 222 + `sX`: array-like of the corresponding SE values 223 224 **Tip** 225 226 If `X` and `sX` are initially arranged as a list of `(x, sx)` doublets, 227 they may be rearranged using `zip()`: 228 229 ```python 230 foo = [(0, 1), (1, 0.5), (2, 0.5)] 231 print(w_avg(*zip(*foo))) # yields: (1.3333333333333333, 0.3333333333333333) 232 ``` 233 ''' 234 X = [ x for x in X ] 235 sX = [ sx for sx in sX ] 236 W = [ sx**-2 for sx in sX ] 237 W = [ w/sum(W) for w in W ] 238 Xavg = sum([ w*x for w,x in zip(W,X) ]) 239 sXavg = sum([ w**2*sx**2 for w,sx in zip(W,sX) ])**.5 240 return Xavg, sXavg 241 242 243def read_csv(filename, sep = ''): 244 ''' 245 Read contents of `filename` in csv format and return a list of dictionaries. 246 247 In the csv string, spaces before and after field separators (`','` by default) 248 are optional. 249 250 **Parameters** 251 252 + `filename`: the csv file to read 253 + `sep`: csv separator delimiting the fields. By default, use `,`, `;`, or `\t`, 254 whichever appers most often in the contents of `filename`. 255 ''' 256 with open(filename) as fid: 257 txt = fid.read() 258 259 if sep == '': 260 sep = sorted(',;\t', key = lambda x: - txt.count(x))[0] 261 txt = [[x.strip() for x in l.split(sep)] for l in txt.splitlines() if l.strip()] 262 return [{k: smart_type(v) for k,v in zip(txt[0], l) if v} for l in txt[1:]] 263 264 265def simulate_single_analysis( 266 sample = 'MYSAMPLE', 267 d13Cwg_VPDB = -4., d18Owg_VSMOW = 26., 268 d13C_VPDB = None, d18O_VPDB = None, 269 D47 = None, D48 = None, D49 = 0., D17O = 0., 270 a47 = 1., b47 = 0., c47 = -0.9, 271 a48 = 1., b48 = 0., c48 = -0.45, 272 Nominal_D47 = None, 273 Nominal_D48 = None, 274 Nominal_d13C_VPDB = None, 275 Nominal_d18O_VPDB = None, 276 ALPHA_18O_ACID_REACTION = None, 277 R13_VPDB = None, 278 R17_VSMOW = None, 279 R18_VSMOW = None, 280 LAMBDA_17 = None, 281 R18_VPDB = None, 282 ): 283 ''' 284 Compute working-gas delta values for a single analysis, assuming a stochastic working 285 gas and a “perfect” measurement (i.e. raw Δ values are identical to absolute values). 286 287 **Parameters** 288 289 + `sample`: sample name 290 + `d13Cwg_VPDB`, `d18Owg_VSMOW`: bulk composition of the working gas 291 (respectively –4 and +26 ‰ by default) 292 + `d13C_VPDB`, `d18O_VPDB`: bulk composition of the carbonate sample 293 + `D47`, `D48`, `D49`, `D17O`: clumped-isotope and oxygen-17 anomalies 294 of the carbonate sample 295 + `Nominal_D47`, `Nominal_D48`: where to lookup Δ47 and 296 Δ48 values if `D47` or `D48` are not specified 297 + `Nominal_d13C_VPDB`, `Nominal_d18O_VPDB`: where to lookup δ13C and 298 δ18O values if `d13C_VPDB` or `d18O_VPDB` are not specified 299 + `ALPHA_18O_ACID_REACTION`: 18O/16O acid fractionation factor 300 + `R13_VPDB`, `R17_VSMOW`, `R18_VSMOW`, `LAMBDA_17`, `R18_VPDB`: oxygen-17 301 correction parameters (by default equal to the `D4xdata` default values) 302 303 Returns a dictionary with fields 304 `['Sample', 'D17O', 'd13Cwg_VPDB', 'd18Owg_VSMOW', 'd45', 'd46', 'd47', 'd48', 'd49']`. 305 ''' 306 307 if Nominal_d13C_VPDB is None: 308 Nominal_d13C_VPDB = D4xdata().Nominal_d13C_VPDB 309 310 if Nominal_d18O_VPDB is None: 311 Nominal_d18O_VPDB = D4xdata().Nominal_d18O_VPDB 312 313 if ALPHA_18O_ACID_REACTION is None: 314 ALPHA_18O_ACID_REACTION = D4xdata().ALPHA_18O_ACID_REACTION 315 316 if R13_VPDB is None: 317 R13_VPDB = D4xdata().R13_VPDB 318 319 if R17_VSMOW is None: 320 R17_VSMOW = D4xdata().R17_VSMOW 321 322 if R18_VSMOW is None: 323 R18_VSMOW = D4xdata().R18_VSMOW 324 325 if LAMBDA_17 is None: 326 LAMBDA_17 = D4xdata().LAMBDA_17 327 328 if R18_VPDB is None: 329 R18_VPDB = D4xdata().R18_VPDB 330 331 R17_VPDB = R17_VSMOW * (R18_VPDB / R18_VSMOW) ** LAMBDA_17 332 333 if Nominal_D47 is None: 334 Nominal_D47 = D47data().Nominal_D47 335 336 if Nominal_D48 is None: 337 Nominal_D48 = D48data().Nominal_D48 338 339 if d13C_VPDB is None: 340 if sample in Nominal_d13C_VPDB: 341 d13C_VPDB = Nominal_d13C_VPDB[sample] 342 else: 343 raise KeyError(f"Sample {sample} is missing d13C_VDP value, and it is not defined in Nominal_d13C_VDP.") 344 345 if d18O_VPDB is None: 346 if sample in Nominal_d18O_VPDB: 347 d18O_VPDB = Nominal_d18O_VPDB[sample] 348 else: 349 raise KeyError(f"Sample {sample} is missing d18O_VPDB value, and it is not defined in Nominal_d18O_VPDB.") 350 351 if D47 is None: 352 if sample in Nominal_D47: 353 D47 = Nominal_D47[sample] 354 else: 355 raise KeyError(f"Sample {sample} is missing D47 value, and it is not defined in Nominal_D47.") 356 357 if D48 is None: 358 if sample in Nominal_D48: 359 D48 = Nominal_D48[sample] 360 else: 361 raise KeyError(f"Sample {sample} is missing D48 value, and it is not defined in Nominal_D48.") 362 363 X = D4xdata() 364 X.R13_VPDB = R13_VPDB 365 X.R17_VSMOW = R17_VSMOW 366 X.R18_VSMOW = R18_VSMOW 367 X.LAMBDA_17 = LAMBDA_17 368 X.R18_VPDB = R18_VPDB 369 X.R17_VPDB = R17_VSMOW * (R18_VPDB / R18_VSMOW)**LAMBDA_17 370 371 R45wg, R46wg, R47wg, R48wg, R49wg = X.compute_isobar_ratios( 372 R13 = R13_VPDB * (1 + d13Cwg_VPDB/1000), 373 R18 = R18_VSMOW * (1 + d18Owg_VSMOW/1000), 374 ) 375 R45, R46, R47, R48, R49 = X.compute_isobar_ratios( 376 R13 = R13_VPDB * (1 + d13C_VPDB/1000), 377 R18 = R18_VPDB * (1 + d18O_VPDB/1000) * ALPHA_18O_ACID_REACTION, 378 D17O=D17O, D47=D47, D48=D48, D49=D49, 379 ) 380 R45stoch, R46stoch, R47stoch, R48stoch, R49stoch = X.compute_isobar_ratios( 381 R13 = R13_VPDB * (1 + d13C_VPDB/1000), 382 R18 = R18_VPDB * (1 + d18O_VPDB/1000) * ALPHA_18O_ACID_REACTION, 383 D17O=D17O, 384 ) 385 386 d45 = 1000 * (R45/R45wg - 1) 387 d46 = 1000 * (R46/R46wg - 1) 388 d47 = 1000 * (R47/R47wg - 1) 389 d48 = 1000 * (R48/R48wg - 1) 390 d49 = 1000 * (R49/R49wg - 1) 391 392 for k in range(3): # dumb iteration to adjust for small changes in d47 393 R47raw = (1 + (a47 * D47 + b47 * d47 + c47)/1000) * R47stoch 394 R48raw = (1 + (a48 * D48 + b48 * d48 + c48)/1000) * R48stoch 395 d47 = 1000 * (R47raw/R47wg - 1) 396 d48 = 1000 * (R48raw/R48wg - 1) 397 398 return dict( 399 Sample = sample, 400 D17O = D17O, 401 d13Cwg_VPDB = d13Cwg_VPDB, 402 d18Owg_VSMOW = d18Owg_VSMOW, 403 d45 = d45, 404 d46 = d46, 405 d47 = d47, 406 d48 = d48, 407 d49 = d49, 408 ) 409 410 411def virtual_data( 412 samples = [], 413 a47 = 1., b47 = 0., c47 = -0.9, 414 a48 = 1., b48 = 0., c48 = -0.45, 415 rD47 = 0.015, rD48 = 0.045, 416 d13Cwg_VPDB = None, d18Owg_VSMOW = None, 417 session = None, 418 Nominal_D47 = None, Nominal_D48 = None, 419 Nominal_d13C_VPDB = None, Nominal_d18O_VPDB = None, 420 ALPHA_18O_ACID_REACTION = None, 421 R13_VPDB = None, 422 R17_VSMOW = None, 423 R18_VSMOW = None, 424 LAMBDA_17 = None, 425 R18_VPDB = None, 426 seed = 0, 427 ): 428 ''' 429 Return list with simulated analyses from a single session. 430 431 **Parameters** 432 433 + `samples`: a list of entries; each entry is a dictionary with the following fields: 434 * `Sample`: the name of the sample 435 * `d13C_VPDB`, `d18O_VPDB`: bulk composition of the carbonate sample 436 * `D47`, `D48`, `D49`, `D17O` (all optional): clumped-isotope and oxygen-17 anomalies of the carbonate sample 437 * `N`: how many analyses to generate for this sample 438 + `a47`: scrambling factor for Δ47 439 + `b47`: compositional nonlinearity for Δ47 440 + `c47`: working gas offset for Δ47 441 + `a48`: scrambling factor for Δ48 442 + `b48`: compositional nonlinearity for Δ48 443 + `c48`: working gas offset for Δ48 444 + `rD47`: analytical repeatability of Δ47 445 + `rD48`: analytical repeatability of Δ48 446 + `d13Cwg_VPDB`, `d18Owg_VSMOW`: bulk composition of the working gas 447 (by default equal to the `simulate_single_analysis` default values) 448 + `session`: name of the session (no name by default) 449 + `Nominal_D47`, `Nominal_D48`: where to lookup Δ47 and Δ48 values 450 if `D47` or `D48` are not specified (by default equal to the `simulate_single_analysis` defaults) 451 + `Nominal_d13C_VPDB`, `Nominal_d18O_VPDB`: where to lookup δ13C and 452 δ18O values if `d13C_VPDB` or `d18O_VPDB` are not specified 453 (by default equal to the `simulate_single_analysis` defaults) 454 + `ALPHA_18O_ACID_REACTION`: 18O/16O acid fractionation factor 455 (by default equal to the `simulate_single_analysis` defaults) 456 + `R13_VPDB`, `R17_VSMOW`, `R18_VSMOW`, `LAMBDA_17`, `R18_VPDB`: oxygen-17 457 correction parameters (by default equal to the `simulate_single_analysis` default) 458 + `seed`: explicitly set to a non-zero value to achieve random but repeatable simulations 459 460 461 Here is an example of using this method to generate an arbitrary combination of 462 anchors and unknowns for a bunch of sessions: 463 464 ```py 465 args = dict( 466 samples = [ 467 dict(Sample = 'ETH-1', N = 4), 468 dict(Sample = 'ETH-2', N = 5), 469 dict(Sample = 'ETH-3', N = 6), 470 dict(Sample = 'FOO', N = 2, 471 d13C_VPDB = -5., d18O_VPDB = -10., 472 D47 = 0.3, D48 = 0.15), 473 ], rD47 = 0.010, rD48 = 0.030) 474 475 session1 = virtual_data(session = 'Session_01', **args, seed = 123) 476 session2 = virtual_data(session = 'Session_02', **args, seed = 1234) 477 session3 = virtual_data(session = 'Session_03', **args, seed = 12345) 478 session4 = virtual_data(session = 'Session_04', **args, seed = 123456) 479 480 D = D47data(session1 + session2 + session3 + session4) 481 482 D.crunch() 483 D.standardize() 484 485 D.table_of_sessions(verbose = True, save_to_file = False) 486 D.table_of_samples(verbose = True, save_to_file = False) 487 D.table_of_analyses(verbose = True, save_to_file = False) 488 ``` 489 490 This should output something like: 491 492 ``` 493 [table_of_sessions] 494 –––––––––– –– –– ––––––––––– –––––––––––– –––––– –––––– –––––– ––––––––––––– –––––––––––––– –––––––––––––– 495 Session Na Nu d13Cwg_VPDB d18Owg_VSMOW r_d13C r_d18O r_D47 a ± SE 1e3 x b ± SE c ± SE 496 –––––––––– –– –– ––––––––––– –––––––––––– –––––– –––––– –––––– ––––––––––––– –––––––––––––– –––––––––––––– 497 Session_01 15 2 -4.000 26.000 0.0000 0.0000 0.0110 0.997 ± 0.017 -0.097 ± 0.244 -0.896 ± 0.006 498 Session_02 15 2 -4.000 26.000 0.0000 0.0000 0.0109 1.002 ± 0.017 -0.110 ± 0.244 -0.901 ± 0.006 499 Session_03 15 2 -4.000 26.000 0.0000 0.0000 0.0107 1.010 ± 0.017 -0.037 ± 0.244 -0.904 ± 0.006 500 Session_04 15 2 -4.000 26.000 0.0000 0.0000 0.0106 1.001 ± 0.017 -0.181 ± 0.244 -0.894 ± 0.006 501 –––––––––– –– –– ––––––––––– –––––––––––– –––––– –––––– –––––– ––––––––––––– –––––––––––––– –––––––––––––– 502 503 [table_of_samples] 504 –––––– –– ––––––––– –––––––––– –––––– –––––– –––––––– –––––– –––––––– 505 Sample N d13C_VPDB d18O_VSMOW D47 SE 95% CL SD p_Levene 506 –––––– –– ––––––––– –––––––––– –––––– –––––– –––––––– –––––– –––––––– 507 ETH-1 16 2.02 37.02 0.2052 0.0079 508 ETH-2 20 -10.17 19.88 0.2085 0.0100 509 ETH-3 24 1.71 37.45 0.6132 0.0105 510 FOO 8 -5.00 28.91 0.2989 0.0040 ± 0.0080 0.0101 0.638 511 –––––– –– ––––––––– –––––––––– –––––– –––––– –––––––– –––––– –––––––– 512 513 [table_of_analyses] 514 ––– –––––––––– –––––– ––––––––––– –––––––––––– ––––––––– ––––––––– –––––––––– –––––––––– –––––––––– –––––––––– –––––––––– ––––––––– ––––––––– ––––––––– –––––––– 515 UID Session Sample d13Cwg_VPDB d18Owg_VSMOW d45 d46 d47 d48 d49 d13C_VPDB d18O_VSMOW D47raw D48raw D49raw D47 516 ––– –––––––––– –––––– ––––––––––– –––––––––––– ––––––––– ––––––––– –––––––––– –––––––––– –––––––––– –––––––––– –––––––––– ––––––––– ––––––––– ––––––––– –––––––– 517 1 Session_01 ETH-1 -4.000 26.000 6.018962 10.747026 16.122986 21.273526 27.780042 2.020000 37.024281 -0.706013 -0.328878 -0.000013 0.192554 518 2 Session_01 ETH-1 -4.000 26.000 6.018962 10.747026 16.130144 21.282615 27.780042 2.020000 37.024281 -0.698974 -0.319981 -0.000013 0.199615 519 3 Session_01 ETH-1 -4.000 26.000 6.018962 10.747026 16.149219 21.299572 27.780042 2.020000 37.024281 -0.680215 -0.303383 -0.000013 0.218429 520 4 Session_01 ETH-1 -4.000 26.000 6.018962 10.747026 16.136616 21.233128 27.780042 2.020000 37.024281 -0.692609 -0.368421 -0.000013 0.205998 521 5 Session_01 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.697171 -12.203054 -18.023381 -10.170000 19.875825 -0.680771 -0.290128 -0.000002 0.215054 522 6 Session_01 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.701124 -12.184422 -18.023381 -10.170000 19.875825 -0.684772 -0.271272 -0.000002 0.211041 523 7 Session_01 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.715105 -12.195251 -18.023381 -10.170000 19.875825 -0.698923 -0.282232 -0.000002 0.196848 524 8 Session_01 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.701529 -12.204963 -18.023381 -10.170000 19.875825 -0.685182 -0.292061 -0.000002 0.210630 525 9 Session_01 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.711420 -12.228478 -18.023381 -10.170000 19.875825 -0.695193 -0.315859 -0.000002 0.200589 526 10 Session_01 ETH-3 -4.000 26.000 5.742374 11.161270 16.666719 22.296486 28.306614 1.710000 37.450394 -0.290459 -0.147284 -0.000014 0.609363 527 11 Session_01 ETH-3 -4.000 26.000 5.742374 11.161270 16.671553 22.291060 28.306614 1.710000 37.450394 -0.285706 -0.152592 -0.000014 0.614130 528 12 Session_01 ETH-3 -4.000 26.000 5.742374 11.161270 16.652854 22.273271 28.306614 1.710000 37.450394 -0.304093 -0.169990 -0.000014 0.595689 529 13 Session_01 ETH-3 -4.000 26.000 5.742374 11.161270 16.684168 22.263156 28.306614 1.710000 37.450394 -0.273302 -0.179883 -0.000014 0.626572 530 14 Session_01 ETH-3 -4.000 26.000 5.742374 11.161270 16.662702 22.253578 28.306614 1.710000 37.450394 -0.294409 -0.189251 -0.000014 0.605401 531 15 Session_01 ETH-3 -4.000 26.000 5.742374 11.161270 16.681957 22.230907 28.306614 1.710000 37.450394 -0.275476 -0.211424 -0.000014 0.624391 532 16 Session_01 FOO -4.000 26.000 -0.840413 2.828738 1.312044 5.395798 4.665655 -5.000000 28.907344 -0.598436 -0.268176 -0.000006 0.298996 533 17 Session_01 FOO -4.000 26.000 -0.840413 2.828738 1.328123 5.307086 4.665655 -5.000000 28.907344 -0.582387 -0.356389 -0.000006 0.315092 534 18 Session_02 ETH-1 -4.000 26.000 6.018962 10.747026 16.122201 21.340606 27.780042 2.020000 37.024281 -0.706785 -0.263217 -0.000013 0.195135 535 19 Session_02 ETH-1 -4.000 26.000 6.018962 10.747026 16.134868 21.305714 27.780042 2.020000 37.024281 -0.694328 -0.297370 -0.000013 0.207564 536 20 Session_02 ETH-1 -4.000 26.000 6.018962 10.747026 16.140008 21.261931 27.780042 2.020000 37.024281 -0.689273 -0.340227 -0.000013 0.212607 537 21 Session_02 ETH-1 -4.000 26.000 6.018962 10.747026 16.135540 21.298472 27.780042 2.020000 37.024281 -0.693667 -0.304459 -0.000013 0.208224 538 22 Session_02 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.701213 -12.202602 -18.023381 -10.170000 19.875825 -0.684862 -0.289671 -0.000002 0.213842 539 23 Session_02 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.685649 -12.190405 -18.023381 -10.170000 19.875825 -0.669108 -0.277327 -0.000002 0.229559 540 24 Session_02 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.719003 -12.257955 -18.023381 -10.170000 19.875825 -0.702869 -0.345692 -0.000002 0.195876 541 25 Session_02 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.700592 -12.204641 -18.023381 -10.170000 19.875825 -0.684233 -0.291735 -0.000002 0.214469 542 26 Session_02 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.720426 -12.214561 -18.023381 -10.170000 19.875825 -0.704308 -0.301774 -0.000002 0.194439 543 27 Session_02 ETH-3 -4.000 26.000 5.742374 11.161270 16.673044 22.262090 28.306614 1.710000 37.450394 -0.284240 -0.180926 -0.000014 0.616730 544 28 Session_02 ETH-3 -4.000 26.000 5.742374 11.161270 16.666542 22.263401 28.306614 1.710000 37.450394 -0.290634 -0.179643 -0.000014 0.610350 545 29 Session_02 ETH-3 -4.000 26.000 5.742374 11.161270 16.680487 22.243486 28.306614 1.710000 37.450394 -0.276921 -0.199121 -0.000014 0.624031 546 30 Session_02 ETH-3 -4.000 26.000 5.742374 11.161270 16.663900 22.245175 28.306614 1.710000 37.450394 -0.293231 -0.197469 -0.000014 0.607759 547 31 Session_02 ETH-3 -4.000 26.000 5.742374 11.161270 16.674379 22.301309 28.306614 1.710000 37.450394 -0.282927 -0.142568 -0.000014 0.618039 548 32 Session_02 ETH-3 -4.000 26.000 5.742374 11.161270 16.660825 22.270466 28.306614 1.710000 37.450394 -0.296255 -0.172733 -0.000014 0.604742 549 33 Session_02 FOO -4.000 26.000 -0.840413 2.828738 1.294076 5.349940 4.665655 -5.000000 28.907344 -0.616369 -0.313776 -0.000006 0.283707 550 34 Session_02 FOO -4.000 26.000 -0.840413 2.828738 1.313775 5.292121 4.665655 -5.000000 28.907344 -0.596708 -0.371269 -0.000006 0.303323 551 35 Session_03 ETH-1 -4.000 26.000 6.018962 10.747026 16.121613 21.259909 27.780042 2.020000 37.024281 -0.707364 -0.342207 -0.000013 0.194934 552 36 Session_03 ETH-1 -4.000 26.000 6.018962 10.747026 16.145714 21.304889 27.780042 2.020000 37.024281 -0.683661 -0.298178 -0.000013 0.218401 553 37 Session_03 ETH-1 -4.000 26.000 6.018962 10.747026 16.126573 21.325093 27.780042 2.020000 37.024281 -0.702485 -0.278401 -0.000013 0.199764 554 38 Session_03 ETH-1 -4.000 26.000 6.018962 10.747026 16.132057 21.323211 27.780042 2.020000 37.024281 -0.697092 -0.280244 -0.000013 0.205104 555 39 Session_03 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.708448 -12.232023 -18.023381 -10.170000 19.875825 -0.692185 -0.319447 -0.000002 0.208915 556 40 Session_03 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.714417 -12.202504 -18.023381 -10.170000 19.875825 -0.698226 -0.289572 -0.000002 0.202934 557 41 Session_03 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.720039 -12.264469 -18.023381 -10.170000 19.875825 -0.703917 -0.352285 -0.000002 0.197300 558 42 Session_03 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.701953 -12.228550 -18.023381 -10.170000 19.875825 -0.685611 -0.315932 -0.000002 0.215423 559 43 Session_03 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.704535 -12.213634 -18.023381 -10.170000 19.875825 -0.688224 -0.300836 -0.000002 0.212837 560 44 Session_03 ETH-3 -4.000 26.000 5.742374 11.161270 16.652920 22.230043 28.306614 1.710000 37.450394 -0.304028 -0.212269 -0.000014 0.594265 561 45 Session_03 ETH-3 -4.000 26.000 5.742374 11.161270 16.691485 22.261017 28.306614 1.710000 37.450394 -0.266106 -0.181975 -0.000014 0.631810 562 46 Session_03 ETH-3 -4.000 26.000 5.742374 11.161270 16.679119 22.305357 28.306614 1.710000 37.450394 -0.278266 -0.138609 -0.000014 0.619771 563 47 Session_03 ETH-3 -4.000 26.000 5.742374 11.161270 16.663623 22.327286 28.306614 1.710000 37.450394 -0.293503 -0.117161 -0.000014 0.604685 564 48 Session_03 ETH-3 -4.000 26.000 5.742374 11.161270 16.678524 22.282103 28.306614 1.710000 37.450394 -0.278851 -0.161352 -0.000014 0.619192 565 49 Session_03 ETH-3 -4.000 26.000 5.742374 11.161270 16.666246 22.283361 28.306614 1.710000 37.450394 -0.290925 -0.160121 -0.000014 0.607238 566 50 Session_03 FOO -4.000 26.000 -0.840413 2.828738 1.309929 5.340249 4.665655 -5.000000 28.907344 -0.600546 -0.323413 -0.000006 0.300148 567 51 Session_03 FOO -4.000 26.000 -0.840413 2.828738 1.317548 5.334102 4.665655 -5.000000 28.907344 -0.592942 -0.329524 -0.000006 0.307676 568 52 Session_04 ETH-1 -4.000 26.000 6.018962 10.747026 16.136865 21.300298 27.780042 2.020000 37.024281 -0.692364 -0.302672 -0.000013 0.204033 569 53 Session_04 ETH-1 -4.000 26.000 6.018962 10.747026 16.133538 21.291260 27.780042 2.020000 37.024281 -0.695637 -0.311519 -0.000013 0.200762 570 54 Session_04 ETH-1 -4.000 26.000 6.018962 10.747026 16.139991 21.319865 27.780042 2.020000 37.024281 -0.689290 -0.283519 -0.000013 0.207107 571 55 Session_04 ETH-1 -4.000 26.000 6.018962 10.747026 16.145748 21.330075 27.780042 2.020000 37.024281 -0.683629 -0.273524 -0.000013 0.212766 572 56 Session_04 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.702989 -12.202762 -18.023381 -10.170000 19.875825 -0.686660 -0.289833 -0.000002 0.204507 573 57 Session_04 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.692830 -12.240287 -18.023381 -10.170000 19.875825 -0.676377 -0.327811 -0.000002 0.214786 574 58 Session_04 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.702899 -12.180291 -18.023381 -10.170000 19.875825 -0.686568 -0.267091 -0.000002 0.204598 575 59 Session_04 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.709282 -12.282257 -18.023381 -10.170000 19.875825 -0.693029 -0.370287 -0.000002 0.198140 576 60 Session_04 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.679330 -12.235994 -18.023381 -10.170000 19.875825 -0.662712 -0.323466 -0.000002 0.228446 577 61 Session_04 ETH-3 -4.000 26.000 5.742374 11.161270 16.695594 22.238663 28.306614 1.710000 37.450394 -0.262066 -0.203838 -0.000014 0.634200 578 62 Session_04 ETH-3 -4.000 26.000 5.742374 11.161270 16.663504 22.286354 28.306614 1.710000 37.450394 -0.293620 -0.157194 -0.000014 0.602656 579 63 Session_04 ETH-3 -4.000 26.000 5.742374 11.161270 16.666457 22.254290 28.306614 1.710000 37.450394 -0.290717 -0.188555 -0.000014 0.605558 580 64 Session_04 ETH-3 -4.000 26.000 5.742374 11.161270 16.666910 22.223232 28.306614 1.710000 37.450394 -0.290271 -0.218930 -0.000014 0.606004 581 65 Session_04 ETH-3 -4.000 26.000 5.742374 11.161270 16.679662 22.257256 28.306614 1.710000 37.450394 -0.277732 -0.185653 -0.000014 0.618539 582 66 Session_04 ETH-3 -4.000 26.000 5.742374 11.161270 16.676768 22.267680 28.306614 1.710000 37.450394 -0.280578 -0.175459 -0.000014 0.615693 583 67 Session_04 FOO -4.000 26.000 -0.840413 2.828738 1.307663 5.317330 4.665655 -5.000000 28.907344 -0.602808 -0.346202 -0.000006 0.290853 584 68 Session_04 FOO -4.000 26.000 -0.840413 2.828738 1.308562 5.331400 4.665655 -5.000000 28.907344 -0.601911 -0.332212 -0.000006 0.291749 585 ––– –––––––––– –––––– ––––––––––– –––––––––––– ––––––––– ––––––––– –––––––––– –––––––––– –––––––––– –––––––––– –––––––––– ––––––––– ––––––––– ––––––––– –––––––– 586 ``` 587 ''' 588 589 kwargs = locals().copy() 590 591 from numpy import random as nprandom 592 if seed: 593 rng = nprandom.default_rng(seed) 594 else: 595 rng = nprandom.default_rng() 596 597 N = sum([s['N'] for s in samples]) 598 errors47 = rng.normal(loc = 0, scale = 1, size = N) # generate random measurement errors 599 errors47 *= rD47 / stdev(errors47) # scale errors to rD47 600 errors48 = rng.normal(loc = 0, scale = 1, size = N) # generate random measurement errors 601 errors48 *= rD48 / stdev(errors48) # scale errors to rD48 602 603 k = 0 604 out = [] 605 for s in samples: 606 kw = {} 607 kw['sample'] = s['Sample'] 608 kw = { 609 **kw, 610 **{var: kwargs[var] 611 for var in [ 612 'd13Cwg_VPDB', 'd18Owg_VSMOW', 'ALPHA_18O_ACID_REACTION', 613 'Nominal_D47', 'Nominal_D48', 'Nominal_d13C_VPDB', 'Nominal_d18O_VPDB', 614 'R13_VPDB', 'R17_VSMOW', 'R18_VSMOW', 'LAMBDA_17', 'R18_VPDB', 615 'a47', 'b47', 'c47', 'a48', 'b48', 'c48', 616 ] 617 if kwargs[var] is not None}, 618 **{var: s[var] 619 for var in ['d13C_VPDB', 'd18O_VPDB', 'D47', 'D48', 'D49', 'D17O'] 620 if var in s}, 621 } 622 623 sN = s['N'] 624 while sN: 625 out.append(simulate_single_analysis(**kw)) 626 out[-1]['d47'] += errors47[k] * a47 627 out[-1]['d48'] += errors48[k] * a48 628 sN -= 1 629 k += 1 630 631 if session is not None: 632 for r in out: 633 r['Session'] = session 634 return out 635 636def table_of_samples( 637 data47 = None, 638 data48 = None, 639 dir = 'output', 640 filename = None, 641 save_to_file = True, 642 print_out = True, 643 output = None, 644 ): 645 ''' 646 Print out, save to disk and/or return a combined table of samples 647 for a pair of `D47data` and `D48data` objects. 648 649 **Parameters** 650 651 + `data47`: `D47data` instance 652 + `data48`: `D48data` instance 653 + `dir`: the directory in which to save the table 654 + `filename`: the name to the csv file to write to 655 + `save_to_file`: whether to save the table to disk 656 + `print_out`: whether to print out the table 657 + `output`: if set to `'pretty'`: return a pretty text table (see `pretty_table()`); 658 if set to `'raw'`: return a list of list of strings 659 (e.g., `[['header1', 'header2'], ['0.1', '0.2']]`) 660 ''' 661 if data47 is None: 662 if data48 is None: 663 raise TypeError("Arguments must include at least one D47data() or D48data() instance.") 664 else: 665 return data48.table_of_samples( 666 dir = dir, 667 filename = filename, 668 save_to_file = save_to_file, 669 print_out = print_out, 670 output = output 671 ) 672 else: 673 if data48 is None: 674 return data47.table_of_samples( 675 dir = dir, 676 filename = filename, 677 save_to_file = save_to_file, 678 print_out = print_out, 679 output = output 680 ) 681 else: 682 out47 = data47.table_of_samples(save_to_file = False, print_out = False, output = 'raw') 683 out48 = data48.table_of_samples(save_to_file = False, print_out = False, output = 'raw') 684 out = transpose_table(transpose_table(out47) + transpose_table(out48)[4:]) 685 686 if save_to_file: 687 if not os.path.exists(dir): 688 os.makedirs(dir) 689 if filename is None: 690 filename = f'D47D48_samples.csv' 691 with open(f'{dir}/{filename}', 'w') as fid: 692 fid.write(make_csv(out)) 693 if print_out: 694 print('\n'+pretty_table(out)) 695 if output == 'raw': 696 return out 697 elif output == 'pretty': 698 return pretty_table(out) 699 700 701def table_of_sessions( 702 data47 = None, 703 data48 = None, 704 dir = 'output', 705 filename = None, 706 save_to_file = True, 707 print_out = True, 708 output = None, 709 ): 710 ''' 711 Print out, save to disk and/or return a combined table of sessions 712 for a pair of `D47data` and `D48data` objects. 713 ***Only applicable if the sessions in `data47` and those in `data48` 714 consist of the exact same sets of analyses.*** 715 716 **Parameters** 717 718 + `data47`: `D47data` instance 719 + `data48`: `D48data` instance 720 + `dir`: the directory in which to save the table 721 + `filename`: the name to the csv file to write to 722 + `save_to_file`: whether to save the table to disk 723 + `print_out`: whether to print out the table 724 + `output`: if set to `'pretty'`: return a pretty text table (see `pretty_table()`); 725 if set to `'raw'`: return a list of list of strings 726 (e.g., `[['header1', 'header2'], ['0.1', '0.2']]`) 727 ''' 728 if data47 is None: 729 if data48 is None: 730 raise TypeError("Arguments must include at least one D47data() or D48data() instance.") 731 else: 732 return data48.table_of_sessions( 733 dir = dir, 734 filename = filename, 735 save_to_file = save_to_file, 736 print_out = print_out, 737 output = output 738 ) 739 else: 740 if data48 is None: 741 return data47.table_of_sessions( 742 dir = dir, 743 filename = filename, 744 save_to_file = save_to_file, 745 print_out = print_out, 746 output = output 747 ) 748 else: 749 out47 = data47.table_of_sessions(save_to_file = False, print_out = False, output = 'raw') 750 out48 = data48.table_of_sessions(save_to_file = False, print_out = False, output = 'raw') 751 for k,x in enumerate(out47[0]): 752 if k>7: 753 out47[0][k] = out47[0][k].replace('a', 'a_47').replace('b', 'b_47').replace('c', 'c_47') 754 out48[0][k] = out48[0][k].replace('a', 'a_48').replace('b', 'b_48').replace('c', 'c_48') 755 out = transpose_table(transpose_table(out47) + transpose_table(out48)[7:]) 756 757 if save_to_file: 758 if not os.path.exists(dir): 759 os.makedirs(dir) 760 if filename is None: 761 filename = f'D47D48_sessions.csv' 762 with open(f'{dir}/{filename}', 'w') as fid: 763 fid.write(make_csv(out)) 764 if print_out: 765 print('\n'+pretty_table(out)) 766 if output == 'raw': 767 return out 768 elif output == 'pretty': 769 return pretty_table(out) 770 771 772def table_of_analyses( 773 data47 = None, 774 data48 = None, 775 dir = 'output', 776 filename = None, 777 save_to_file = True, 778 print_out = True, 779 output = None, 780 ): 781 ''' 782 Print out, save to disk and/or return a combined table of analyses 783 for a pair of `D47data` and `D48data` objects. 784 785 If the sessions in `data47` and those in `data48` do not consist of 786 the exact same sets of analyses, the table will have two columns 787 `Session_47` and `Session_48` instead of a single `Session` column. 788 789 **Parameters** 790 791 + `data47`: `D47data` instance 792 + `data48`: `D48data` instance 793 + `dir`: the directory in which to save the table 794 + `filename`: the name to the csv file to write to 795 + `save_to_file`: whether to save the table to disk 796 + `print_out`: whether to print out the table 797 + `output`: if set to `'pretty'`: return a pretty text table (see `pretty_table()`); 798 if set to `'raw'`: return a list of list of strings 799 (e.g., `[['header1', 'header2'], ['0.1', '0.2']]`) 800 ''' 801 if data47 is None: 802 if data48 is None: 803 raise TypeError("Arguments must include at least one D47data() or D48data() instance.") 804 else: 805 return data48.table_of_analyses( 806 dir = dir, 807 filename = filename, 808 save_to_file = save_to_file, 809 print_out = print_out, 810 output = output 811 ) 812 else: 813 if data48 is None: 814 return data47.table_of_analyses( 815 dir = dir, 816 filename = filename, 817 save_to_file = save_to_file, 818 print_out = print_out, 819 output = output 820 ) 821 else: 822 out47 = data47.table_of_analyses(save_to_file = False, print_out = False, output = 'raw') 823 out48 = data48.table_of_analyses(save_to_file = False, print_out = False, output = 'raw') 824 825 if [l[1] for l in out47[1:]] == [l[1] for l in out48[1:]]: # if sessions are identical 826 out = transpose_table(transpose_table(out47) + transpose_table(out48)[-1:]) 827 else: 828 out47[0][1] = 'Session_47' 829 out48[0][1] = 'Session_48' 830 out47 = transpose_table(out47) 831 out48 = transpose_table(out48) 832 out = transpose_table(out47[:2] + out48[1:2] + out47[2:] + out48[-1:]) 833 834 if save_to_file: 835 if not os.path.exists(dir): 836 os.makedirs(dir) 837 if filename is None: 838 filename = f'D47D48_sessions.csv' 839 with open(f'{dir}/{filename}', 'w') as fid: 840 fid.write(make_csv(out)) 841 if print_out: 842 print('\n'+pretty_table(out)) 843 if output == 'raw': 844 return out 845 elif output == 'pretty': 846 return pretty_table(out) 847 848 849class D4xdata(list): 850 ''' 851 Store and process data for a large set of Δ47 and/or Δ48 852 analyses, usually comprising more than one analytical session. 853 ''' 854 855 ### 17O CORRECTION PARAMETERS 856 R13_VPDB = 0.01118 # (Chang & Li, 1990) 857 ''' 858 Absolute (13C/12C) ratio of VPDB. 859 By default equal to 0.01118 ([Chang & Li, 1990](http://www.cnki.com.cn/Article/CJFDTotal-JXTW199004006.htm)) 860 ''' 861 862 R18_VSMOW = 0.0020052 # (Baertschi, 1976) 863 ''' 864 Absolute (18O/16C) ratio of VSMOW. 865 By default equal to 0.0020052 ([Baertschi, 1976](https://doi.org/10.1016/0012-821X(76)90115-1)) 866 ''' 867 868 LAMBDA_17 = 0.528 # (Barkan & Luz, 2005) 869 ''' 870 Mass-dependent exponent for triple oxygen isotopes. 871 By default equal to 0.528 ([Barkan & Luz, 2005](https://doi.org/10.1002/rcm.2250)) 872 ''' 873 874 R17_VSMOW = 0.00038475 # (Assonov & Brenninkmeijer, 2003, rescaled to R13_VPDB) 875 ''' 876 Absolute (17O/16C) ratio of VSMOW. 877 By default equal to 0.00038475 878 ([Assonov & Brenninkmeijer, 2003](https://dx.doi.org/10.1002/rcm.1011), 879 rescaled to `R13_VPDB`) 880 ''' 881 882 R18_VPDB = R18_VSMOW * 1.03092 883 ''' 884 Absolute (18O/16C) ratio of VPDB. 885 By definition equal to `R18_VSMOW * 1.03092`. 886 ''' 887 888 R17_VPDB = R17_VSMOW * 1.03092 ** LAMBDA_17 889 ''' 890 Absolute (17O/16C) ratio of VPDB. 891 By definition equal to `R17_VSMOW * 1.03092 ** LAMBDA_17`. 892 ''' 893 894 LEVENE_REF_SAMPLE = 'ETH-3' 895 ''' 896 After the Δ4x standardization step, each sample is tested to 897 assess whether the Δ4x variance within all analyses for that 898 sample differs significantly from that observed for a given reference 899 sample (using [Levene's test](https://en.wikipedia.org/wiki/Levene%27s_test), 900 which yields a p-value corresponding to the null hypothesis that the 901 underlying variances are equal). 902 903 `LEVENE_REF_SAMPLE` (by default equal to `'ETH-3'`) specifies which 904 sample should be used as a reference for this test. 905 ''' 906 907 ALPHA_18O_ACID_REACTION = round(np.exp(3.59 / (90 + 273.15) - 1.79e-3), 6) # (Kim et al., 2007, calcite) 908 ''' 909 Specifies the 18O/16O fractionation factor generally applicable 910 to acid reactions in the dataset. Currently used by `D4xdata.wg()`, 911 `D4xdata.standardize_d13C`, and `D4xdata.standardize_d18O`. 912 913 By default equal to 1.008129 (calcite reacted at 90 °C, 914 [Kim et al., 2007](https://dx.doi.org/10.1016/j.chemgeo.2007.08.005)). 915 ''' 916 917 Nominal_d13C_VPDB = { 918 'ETH-1': 2.02, 919 'ETH-2': -10.17, 920 'ETH-3': 1.71, 921 } # (Bernasconi et al., 2018) 922 ''' 923 Nominal δ13C_VPDB values assigned to carbonate standards, used by 924 `D4xdata.standardize_d13C()`. 925 926 By default equal to `{'ETH-1': 2.02, 'ETH-2': -10.17, 'ETH-3': 1.71}` after 927 [Bernasconi et al. (2018)](https://doi.org/10.1029/2017GC007385). 928 ''' 929 930 Nominal_d18O_VPDB = { 931 'ETH-1': -2.19, 932 'ETH-2': -18.69, 933 'ETH-3': -1.78, 934 } # (Bernasconi et al., 2018) 935 ''' 936 Nominal δ18O_VPDB values assigned to carbonate standards, used by 937 `D4xdata.standardize_d18O()`. 938 939 By default equal to `{'ETH-1': -2.19, 'ETH-2': -18.69, 'ETH-3': -1.78}` after 940 [Bernasconi et al. (2018)](https://doi.org/10.1029/2017GC007385). 941 ''' 942 943 d13C_STANDARDIZATION_METHOD = '2pt' 944 ''' 945 Method by which to standardize δ13C values: 946 947 + `none`: do not apply any δ13C standardization. 948 + `'1pt'`: within each session, offset all initial δ13C values so as to 949 minimize the difference between final δ13C_VPDB values and 950 `Nominal_d13C_VPDB` (averaged over all analyses for which `Nominal_d13C_VPDB` is defined). 951 + `'2pt'`: within each session, apply a affine trasformation to all δ13C 952 values so as to minimize the difference between final δ13C_VPDB 953 values and `Nominal_d13C_VPDB` (averaged over all analyses for which `Nominal_d13C_VPDB` 954 is defined). 955 ''' 956 957 d18O_STANDARDIZATION_METHOD = '2pt' 958 ''' 959 Method by which to standardize δ18O values: 960 961 + `none`: do not apply any δ18O standardization. 962 + `'1pt'`: within each session, offset all initial δ18O values so as to 963 minimize the difference between final δ18O_VPDB values and 964 `Nominal_d18O_VPDB` (averaged over all analyses for which `Nominal_d18O_VPDB` is defined). 965 + `'2pt'`: within each session, apply a affine trasformation to all δ18O 966 values so as to minimize the difference between final δ18O_VPDB 967 values and `Nominal_d18O_VPDB` (averaged over all analyses for which `Nominal_d18O_VPDB` 968 is defined). 969 ''' 970 971 def __init__(self, l = [], mass = '47', logfile = '', session = 'mySession', verbose = False): 972 ''' 973 **Parameters** 974 975 + `l`: a list of dictionaries, with each dictionary including at least the keys 976 `Sample`, `d45`, `d46`, and `d47` or `d48`. 977 + `mass`: `'47'` or `'48'` 978 + `logfile`: if specified, write detailed logs to this file path when calling `D4xdata` methods. 979 + `session`: define session name for analyses without a `Session` key 980 + `verbose`: if `True`, print out detailed logs when calling `D4xdata` methods. 981 982 Returns a `D4xdata` object derived from `list`. 983 ''' 984 self._4x = mass 985 self.verbose = verbose 986 self.prefix = 'D4xdata' 987 self.logfile = logfile 988 list.__init__(self, l) 989 self.Nf = None 990 self.repeatability = {} 991 self.refresh(session = session) 992 993 994 def make_verbal(oldfun): 995 ''' 996 Decorator: allow temporarily changing `self.prefix` and overriding `self.verbose`. 997 ''' 998 @wraps(oldfun) 999 def newfun(*args, verbose = '', **kwargs): 1000 myself = args[0] 1001 oldprefix = myself.prefix 1002 myself.prefix = oldfun.__name__ 1003 if verbose != '': 1004 oldverbose = myself.verbose 1005 myself.verbose = verbose 1006 out = oldfun(*args, **kwargs) 1007 myself.prefix = oldprefix 1008 if verbose != '': 1009 myself.verbose = oldverbose 1010 return out 1011 return newfun 1012 1013 1014 def msg(self, txt): 1015 ''' 1016 Log a message to `self.logfile`, and print it out if `verbose = True` 1017 ''' 1018 self.log(txt) 1019 if self.verbose: 1020 print(f'{f"[{self.prefix}]":<16} {txt}') 1021 1022 1023 def vmsg(self, txt): 1024 ''' 1025 Log a message to `self.logfile` and print it out 1026 ''' 1027 self.log(txt) 1028 print(txt) 1029 1030 1031 def log(self, *txts): 1032 ''' 1033 Log a message to `self.logfile` 1034 ''' 1035 if self.logfile: 1036 with open(self.logfile, 'a') as fid: 1037 for txt in txts: 1038 fid.write(f'\n{dt.now().strftime("%Y-%m-%d %H:%M:%S")} {f"[{self.prefix}]":<16} {txt}') 1039 1040 1041 def refresh(self, session = 'mySession'): 1042 ''' 1043 Update `self.sessions`, `self.samples`, `self.anchors`, and `self.unknowns`. 1044 ''' 1045 self.fill_in_missing_info(session = session) 1046 self.refresh_sessions() 1047 self.refresh_samples() 1048 1049 1050 def refresh_sessions(self): 1051 ''' 1052 Update `self.sessions` and set `scrambling_drift`, `slope_drift`, and `wg_drift` 1053 to `False` for all sessions. 1054 ''' 1055 self.sessions = { 1056 s: {'data': [r for r in self if r['Session'] == s]} 1057 for s in sorted({r['Session'] for r in self}) 1058 } 1059 for s in self.sessions: 1060 self.sessions[s]['scrambling_drift'] = False 1061 self.sessions[s]['slope_drift'] = False 1062 self.sessions[s]['wg_drift'] = False 1063 self.sessions[s]['d13C_standardization_method'] = self.d13C_STANDARDIZATION_METHOD 1064 self.sessions[s]['d18O_standardization_method'] = self.d18O_STANDARDIZATION_METHOD 1065 1066 1067 def refresh_samples(self): 1068 ''' 1069 Define `self.samples`, `self.anchors`, and `self.unknowns`. 1070 ''' 1071 self.samples = { 1072 s: {'data': [r for r in self if r['Sample'] == s]} 1073 for s in sorted({r['Sample'] for r in self}) 1074 } 1075 self.anchors = {s: self.samples[s] for s in self.samples if s in self.Nominal_D4x} 1076 self.unknowns = {s: self.samples[s] for s in self.samples if s not in self.Nominal_D4x} 1077 1078 1079 def read(self, filename, sep = '', session = ''): 1080 ''' 1081 Read file in csv format to load data into a `D47data` object. 1082 1083 In the csv file, spaces before and after field separators (`','` by default) 1084 are optional. Each line corresponds to a single analysis. 1085 1086 The required fields are: 1087 1088 + `UID`: a unique identifier 1089 + `Session`: an identifier for the analytical session 1090 + `Sample`: a sample identifier 1091 + `d45`, `d46`, and at least one of `d47` or `d48`: the working-gas delta values 1092 1093 Independently known oxygen-17 anomalies may be provided as `D17O` (in ‰ relative to 1094 VSMOW, λ = `self.LAMBDA_17`), and are otherwise assumed to be zero. Working-gas deltas `d47`, `d48` 1095 and `d49` are optional, and set to NaN by default. 1096 1097 **Parameters** 1098 1099 + `fileneme`: the path of the file to read 1100 + `sep`: csv separator delimiting the fields 1101 + `session`: set `Session` field to this string for all analyses 1102 ''' 1103 with open(filename) as fid: 1104 self.input(fid.read(), sep = sep, session = session) 1105 1106 1107 def input(self, txt, sep = '', session = ''): 1108 ''' 1109 Read `txt` string in csv format to load analysis data into a `D47data` object. 1110 1111 In the csv string, spaces before and after field separators (`','` by default) 1112 are optional. Each line corresponds to a single analysis. 1113 1114 The required fields are: 1115 1116 + `UID`: a unique identifier 1117 + `Session`: an identifier for the analytical session 1118 + `Sample`: a sample identifier 1119 + `d45`, `d46`, and at least one of `d47` or `d48`: the working-gas delta values 1120 1121 Independently known oxygen-17 anomalies may be provided as `D17O` (in ‰ relative to 1122 VSMOW, λ = `self.LAMBDA_17`), and are otherwise assumed to be zero. Working-gas deltas `d47`, `d48` 1123 and `d49` are optional, and set to NaN by default. 1124 1125 **Parameters** 1126 1127 + `txt`: the csv string to read 1128 + `sep`: csv separator delimiting the fields. By default, use `,`, `;`, or `\t`, 1129 whichever appers most often in `txt`. 1130 + `session`: set `Session` field to this string for all analyses 1131 ''' 1132 if sep == '': 1133 sep = sorted(',;\t', key = lambda x: - txt.count(x))[0] 1134 txt = [[x.strip() for x in l.split(sep)] for l in txt.splitlines() if l.strip()] 1135 data = [{k: v if k in ['UID', 'Session', 'Sample'] else smart_type(v) for k,v in zip(txt[0], l) if v != ''} for l in txt[1:]] 1136 1137 if session != '': 1138 for r in data: 1139 r['Session'] = session 1140 1141 self += data 1142 self.refresh() 1143 1144 1145 @make_verbal 1146 def wg(self, samples = None, a18_acid = None): 1147 ''' 1148 Compute bulk composition of the working gas for each session based on 1149 the carbonate standards defined in both `self.Nominal_d13C_VPDB` and 1150 `self.Nominal_d18O_VPDB`. 1151 ''' 1152 1153 self.msg('Computing WG composition:') 1154 1155 if a18_acid is None: 1156 a18_acid = self.ALPHA_18O_ACID_REACTION 1157 if samples is None: 1158 samples = [s for s in self.Nominal_d13C_VPDB if s in self.Nominal_d18O_VPDB] 1159 1160 assert a18_acid, f'Acid fractionation factor should not be zero.' 1161 1162 samples = [s for s in samples if s in self.Nominal_d13C_VPDB and s in self.Nominal_d18O_VPDB] 1163 R45R46_standards = {} 1164 for sample in samples: 1165 d13C_vpdb = self.Nominal_d13C_VPDB[sample] 1166 d18O_vpdb = self.Nominal_d18O_VPDB[sample] 1167 R13_s = self.R13_VPDB * (1 + d13C_vpdb / 1000) 1168 R17_s = self.R17_VPDB * ((1 + d18O_vpdb / 1000) * a18_acid) ** self.LAMBDA_17 1169 R18_s = self.R18_VPDB * (1 + d18O_vpdb / 1000) * a18_acid 1170 1171 C12_s = 1 / (1 + R13_s) 1172 C13_s = R13_s / (1 + R13_s) 1173 C16_s = 1 / (1 + R17_s + R18_s) 1174 C17_s = R17_s / (1 + R17_s + R18_s) 1175 C18_s = R18_s / (1 + R17_s + R18_s) 1176 1177 C626_s = C12_s * C16_s ** 2 1178 C627_s = 2 * C12_s * C16_s * C17_s 1179 C628_s = 2 * C12_s * C16_s * C18_s 1180 C636_s = C13_s * C16_s ** 2 1181 C637_s = 2 * C13_s * C16_s * C17_s 1182 C727_s = C12_s * C17_s ** 2 1183 1184 R45_s = (C627_s + C636_s) / C626_s 1185 R46_s = (C628_s + C637_s + C727_s) / C626_s 1186 R45R46_standards[sample] = (R45_s, R46_s) 1187 1188 for s in self.sessions: 1189 db = [r for r in self.sessions[s]['data'] if r['Sample'] in samples] 1190 assert db, f'No sample from {samples} found in session "{s}".' 1191# dbsamples = sorted({r['Sample'] for r in db}) 1192 1193 X = [r['d45'] for r in db] 1194 Y = [R45R46_standards[r['Sample']][0] for r in db] 1195 x1, x2 = np.min(X), np.max(X) 1196 1197 if x1 < x2: 1198 wgcoord = x1/(x1-x2) 1199 else: 1200 wgcoord = 999 1201 1202 if wgcoord < -.5 or wgcoord > 1.5: 1203 # unreasonable to extrapolate to d45 = 0 1204 R45_wg = np.mean([y/(1+x/1000) for x,y in zip(X,Y)]) 1205 else : 1206 # d45 = 0 is reasonably well bracketed 1207 R45_wg = np.polyfit(X, Y, 1)[1] 1208 1209 X = [r['d46'] for r in db] 1210 Y = [R45R46_standards[r['Sample']][1] for r in db] 1211 x1, x2 = np.min(X), np.max(X) 1212 1213 if x1 < x2: 1214 wgcoord = x1/(x1-x2) 1215 else: 1216 wgcoord = 999 1217 1218 if wgcoord < -.5 or wgcoord > 1.5: 1219 # unreasonable to extrapolate to d46 = 0 1220 R46_wg = np.mean([y/(1+x/1000) for x,y in zip(X,Y)]) 1221 else : 1222 # d46 = 0 is reasonably well bracketed 1223 R46_wg = np.polyfit(X, Y, 1)[1] 1224 1225 d13Cwg_VPDB, d18Owg_VSMOW = self.compute_bulk_delta(R45_wg, R46_wg) 1226 1227 self.msg(f'Session {s} WG: δ13C_VPDB = {d13Cwg_VPDB:.3f} δ18O_VSMOW = {d18Owg_VSMOW:.3f}') 1228 1229 self.sessions[s]['d13Cwg_VPDB'] = d13Cwg_VPDB 1230 self.sessions[s]['d18Owg_VSMOW'] = d18Owg_VSMOW 1231 for r in self.sessions[s]['data']: 1232 r['d13Cwg_VPDB'] = d13Cwg_VPDB 1233 r['d18Owg_VSMOW'] = d18Owg_VSMOW 1234 1235 1236 def compute_bulk_delta(self, R45, R46, D17O = 0): 1237 ''' 1238 Compute δ13C_VPDB and δ18O_VSMOW, 1239 by solving the generalized form of equation (17) from 1240 [Brand et al. (2010)](https://doi.org/10.1351/PAC-REP-09-01-05), 1241 assuming that δ18O_VSMOW is not too big (0 ± 50 ‰) and 1242 solving the corresponding second-order Taylor polynomial. 1243 (Appendix A of [Daëron et al., 2016](https://doi.org/10.1016/j.chemgeo.2016.08.014)) 1244 ''' 1245 1246 K = np.exp(D17O / 1000) * self.R17_VSMOW * self.R18_VSMOW ** -self.LAMBDA_17 1247 1248 A = -3 * K ** 2 * self.R18_VSMOW ** (2 * self.LAMBDA_17) 1249 B = 2 * K * R45 * self.R18_VSMOW ** self.LAMBDA_17 1250 C = 2 * self.R18_VSMOW 1251 D = -R46 1252 1253 aa = A * self.LAMBDA_17 * (2 * self.LAMBDA_17 - 1) + B * self.LAMBDA_17 * (self.LAMBDA_17 - 1) / 2 1254 bb = 2 * A * self.LAMBDA_17 + B * self.LAMBDA_17 + C 1255 cc = A + B + C + D 1256 1257 d18O_VSMOW = 1000 * (-bb + (bb ** 2 - 4 * aa * cc) ** .5) / (2 * aa) 1258 1259 R18 = (1 + d18O_VSMOW / 1000) * self.R18_VSMOW 1260 R17 = K * R18 ** self.LAMBDA_17 1261 R13 = R45 - 2 * R17 1262 1263 d13C_VPDB = 1000 * (R13 / self.R13_VPDB - 1) 1264 1265 return d13C_VPDB, d18O_VSMOW 1266 1267 1268 @make_verbal 1269 def crunch(self, verbose = ''): 1270 ''' 1271 Compute bulk composition and raw clumped isotope anomalies for all analyses. 1272 ''' 1273 for r in self: 1274 self.compute_bulk_and_clumping_deltas(r) 1275 self.standardize_d13C() 1276 self.standardize_d18O() 1277 self.msg(f"Crunched {len(self)} analyses.") 1278 1279 1280 def fill_in_missing_info(self, session = 'mySession'): 1281 ''' 1282 Fill in optional fields with default values 1283 ''' 1284 for i,r in enumerate(self): 1285 if 'D17O' not in r: 1286 r['D17O'] = 0. 1287 if 'UID' not in r: 1288 r['UID'] = f'{i+1}' 1289 if 'Session' not in r: 1290 r['Session'] = session 1291 for k in ['d47', 'd48', 'd49']: 1292 if k not in r: 1293 r[k] = np.nan 1294 1295 1296 def standardize_d13C(self): 1297 ''' 1298 Perform δ13C standadization within each session `s` according to 1299 `self.sessions[s]['d13C_standardization_method']`, which is defined by default 1300 by `D47data.refresh_sessions()`as equal to `self.d13C_STANDARDIZATION_METHOD`, but 1301 may be redefined abitrarily at a later stage. 1302 ''' 1303 for s in self.sessions: 1304 if self.sessions[s]['d13C_standardization_method'] in ['1pt', '2pt']: 1305 XY = [(r['d13C_VPDB'], self.Nominal_d13C_VPDB[r['Sample']]) for r in self.sessions[s]['data'] if r['Sample'] in self.Nominal_d13C_VPDB] 1306 X,Y = zip(*XY) 1307 if self.sessions[s]['d13C_standardization_method'] == '1pt': 1308 offset = np.mean(Y) - np.mean(X) 1309 for r in self.sessions[s]['data']: 1310 r['d13C_VPDB'] += offset 1311 elif self.sessions[s]['d13C_standardization_method'] == '2pt': 1312 a,b = np.polyfit(X,Y,1) 1313 for r in self.sessions[s]['data']: 1314 r['d13C_VPDB'] = a * r['d13C_VPDB'] + b 1315 1316 def standardize_d18O(self): 1317 ''' 1318 Perform δ18O standadization within each session `s` according to 1319 `self.ALPHA_18O_ACID_REACTION` and `self.sessions[s]['d18O_standardization_method']`, 1320 which is defined by default by `D47data.refresh_sessions()`as equal to 1321 `self.d18O_STANDARDIZATION_METHOD`, but may be redefined abitrarily at a later stage. 1322 ''' 1323 for s in self.sessions: 1324 if self.sessions[s]['d18O_standardization_method'] in ['1pt', '2pt']: 1325 XY = [(r['d18O_VSMOW'], self.Nominal_d18O_VPDB[r['Sample']]) for r in self.sessions[s]['data'] if r['Sample'] in self.Nominal_d18O_VPDB] 1326 X,Y = zip(*XY) 1327 Y = [(1000+y) * self.R18_VPDB * self.ALPHA_18O_ACID_REACTION / self.R18_VSMOW - 1000 for y in Y] 1328 if self.sessions[s]['d18O_standardization_method'] == '1pt': 1329 offset = np.mean(Y) - np.mean(X) 1330 for r in self.sessions[s]['data']: 1331 r['d18O_VSMOW'] += offset 1332 elif self.sessions[s]['d18O_standardization_method'] == '2pt': 1333 a,b = np.polyfit(X,Y,1) 1334 for r in self.sessions[s]['data']: 1335 r['d18O_VSMOW'] = a * r['d18O_VSMOW'] + b 1336 1337 1338 def compute_bulk_and_clumping_deltas(self, r): 1339 ''' 1340 Compute δ13C_VPDB, δ18O_VSMOW, and raw Δ47, Δ48, Δ49 values for a single analysis `r`. 1341 ''' 1342 1343 # Compute working gas R13, R18, and isobar ratios 1344 R13_wg = self.R13_VPDB * (1 + r['d13Cwg_VPDB'] / 1000) 1345 R18_wg = self.R18_VSMOW * (1 + r['d18Owg_VSMOW'] / 1000) 1346 R45_wg, R46_wg, R47_wg, R48_wg, R49_wg = self.compute_isobar_ratios(R13_wg, R18_wg) 1347 1348 # Compute analyte isobar ratios 1349 R45 = (1 + r['d45'] / 1000) * R45_wg 1350 R46 = (1 + r['d46'] / 1000) * R46_wg 1351 R47 = (1 + r['d47'] / 1000) * R47_wg 1352 R48 = (1 + r['d48'] / 1000) * R48_wg 1353 R49 = (1 + r['d49'] / 1000) * R49_wg 1354 1355 r['d13C_VPDB'], r['d18O_VSMOW'] = self.compute_bulk_delta(R45, R46, D17O = r['D17O']) 1356 R13 = (1 + r['d13C_VPDB'] / 1000) * self.R13_VPDB 1357 R18 = (1 + r['d18O_VSMOW'] / 1000) * self.R18_VSMOW 1358 1359 # Compute stochastic isobar ratios of the analyte 1360 R45stoch, R46stoch, R47stoch, R48stoch, R49stoch = self.compute_isobar_ratios( 1361 R13, R18, D17O = r['D17O'] 1362 ) 1363 1364 # Check that R45/R45stoch and R46/R46stoch are undistinguishable from 1, 1365 # and raise a warning if the corresponding anomalies exceed 0.02 ppm. 1366 if (R45 / R45stoch - 1) > 5e-8: 1367 self.vmsg(f'This is unexpected: R45/R45stoch - 1 = {1e6 * (R45 / R45stoch - 1):.3f} ppm') 1368 if (R46 / R46stoch - 1) > 5e-8: 1369 self.vmsg(f'This is unexpected: R46/R46stoch - 1 = {1e6 * (R46 / R46stoch - 1):.3f} ppm') 1370 1371 # Compute raw clumped isotope anomalies 1372 r['D47raw'] = 1000 * (R47 / R47stoch - 1) 1373 r['D48raw'] = 1000 * (R48 / R48stoch - 1) 1374 r['D49raw'] = 1000 * (R49 / R49stoch - 1) 1375 1376 1377 def compute_isobar_ratios(self, R13, R18, D17O=0, D47=0, D48=0, D49=0): 1378 ''' 1379 Compute isobar ratios for a sample with isotopic ratios `R13` and `R18`, 1380 optionally accounting for non-zero values of Δ17O (`D17O`) and clumped isotope 1381 anomalies (`D47`, `D48`, `D49`), all expressed in permil. 1382 ''' 1383 1384 # Compute R17 1385 R17 = self.R17_VSMOW * np.exp(D17O / 1000) * (R18 / self.R18_VSMOW) ** self.LAMBDA_17 1386 1387 # Compute isotope concentrations 1388 C12 = (1 + R13) ** -1 1389 C13 = C12 * R13 1390 C16 = (1 + R17 + R18) ** -1 1391 C17 = C16 * R17 1392 C18 = C16 * R18 1393 1394 # Compute stochastic isotopologue concentrations 1395 C626 = C16 * C12 * C16 1396 C627 = C16 * C12 * C17 * 2 1397 C628 = C16 * C12 * C18 * 2 1398 C636 = C16 * C13 * C16 1399 C637 = C16 * C13 * C17 * 2 1400 C638 = C16 * C13 * C18 * 2 1401 C727 = C17 * C12 * C17 1402 C728 = C17 * C12 * C18 * 2 1403 C737 = C17 * C13 * C17 1404 C738 = C17 * C13 * C18 * 2 1405 C828 = C18 * C12 * C18 1406 C838 = C18 * C13 * C18 1407 1408 # Compute stochastic isobar ratios 1409 R45 = (C636 + C627) / C626 1410 R46 = (C628 + C637 + C727) / C626 1411 R47 = (C638 + C728 + C737) / C626 1412 R48 = (C738 + C828) / C626 1413 R49 = C838 / C626 1414 1415 # Account for stochastic anomalies 1416 R47 *= 1 + D47 / 1000 1417 R48 *= 1 + D48 / 1000 1418 R49 *= 1 + D49 / 1000 1419 1420 # Return isobar ratios 1421 return R45, R46, R47, R48, R49 1422 1423 1424 def split_samples(self, samples_to_split = 'all', grouping = 'by_session'): 1425 ''' 1426 Split unknown samples by UID (treat all analyses as different samples) 1427 or by session (treat analyses of a given sample in different sessions as 1428 different samples). 1429 1430 **Parameters** 1431 1432 + `samples_to_split`: a list of samples to split, e.g., `['IAEA-C1', 'IAEA-C2']` 1433 + `grouping`: `by_uid` | `by_session` 1434 ''' 1435 if samples_to_split == 'all': 1436 samples_to_split = [s for s in self.unknowns] 1437 gkeys = {'by_uid':'UID', 'by_session':'Session'} 1438 self.grouping = grouping.lower() 1439 if self.grouping in gkeys: 1440 gkey = gkeys[self.grouping] 1441 for r in self: 1442 if r['Sample'] in samples_to_split: 1443 r['Sample_original'] = r['Sample'] 1444 r['Sample'] = f"{r['Sample']}__{r[gkey]}" 1445 elif r['Sample'] in self.unknowns: 1446 r['Sample_original'] = r['Sample'] 1447 self.refresh_samples() 1448 1449 1450 def unsplit_samples(self, tables = False): 1451 ''' 1452 Reverse the effects of `D47data.split_samples()`. 1453 1454 This should only be used after `D4xdata.standardize()` with `method='pooled'`. 1455 1456 After `D4xdata.standardize()` with `method='indep_sessions'`, one should 1457 probably use `D4xdata.combine_samples()` instead to reverse the effects of 1458 `D47data.split_samples()` with `grouping='by_uid'`, or `w_avg()` to reverse the 1459 effects of `D47data.split_samples()` with `grouping='by_sessions'` (because in 1460 that case session-averaged Δ4x values are statistically independent). 1461 ''' 1462 unknowns_old = sorted({s for s in self.unknowns}) 1463 CM_old = self.standardization.covar[:,:] 1464 VD_old = self.standardization.params.valuesdict().copy() 1465 vars_old = self.standardization.var_names 1466 1467 unknowns_new = sorted({r['Sample_original'] for r in self if 'Sample_original' in r}) 1468 1469 Ns = len(vars_old) - len(unknowns_old) 1470 vars_new = vars_old[:Ns] + [f'D{self._4x}_{pf(u)}' for u in unknowns_new] 1471 VD_new = {k: VD_old[k] for k in vars_old[:Ns]} 1472 1473 W = np.zeros((len(vars_new), len(vars_old))) 1474 W[:Ns,:Ns] = np.eye(Ns) 1475 for u in unknowns_new: 1476 splits = sorted({r['Sample'] for r in self if 'Sample_original' in r and r['Sample_original'] == u}) 1477 if self.grouping == 'by_session': 1478 weights = [self.samples[s][f'SE_D{self._4x}']**-2 for s in splits] 1479 elif self.grouping == 'by_uid': 1480 weights = [1 for s in splits] 1481 sw = sum(weights) 1482 weights = [w/sw for w in weights] 1483 W[vars_new.index(f'D{self._4x}_{pf(u)}'),[vars_old.index(f'D{self._4x}_{pf(s)}') for s in splits]] = weights[:] 1484 1485 CM_new = W @ CM_old @ W.T 1486 V = W @ np.array([[VD_old[k]] for k in vars_old]) 1487 VD_new = {k:v[0] for k,v in zip(vars_new, V)} 1488 1489 self.standardization.covar = CM_new 1490 self.standardization.params.valuesdict = lambda : VD_new 1491 self.standardization.var_names = vars_new 1492 1493 for r in self: 1494 if r['Sample'] in self.unknowns: 1495 r['Sample_split'] = r['Sample'] 1496 r['Sample'] = r['Sample_original'] 1497 1498 self.refresh_samples() 1499 self.consolidate_samples() 1500 self.repeatabilities() 1501 1502 if tables: 1503 self.table_of_analyses() 1504 self.table_of_samples() 1505 1506 def assign_timestamps(self): 1507 ''' 1508 Assign a time field `t` of type `float` to each analysis. 1509 1510 If `TimeTag` is one of the data fields, `t` is equal within a given session 1511 to `TimeTag` minus the mean value of `TimeTag` for that session. 1512 Otherwise, `TimeTag` is by default equal to the index of each analysis 1513 in the dataset and `t` is defined as above. 1514 ''' 1515 for session in self.sessions: 1516 sdata = self.sessions[session]['data'] 1517 try: 1518 t0 = np.mean([r['TimeTag'] for r in sdata]) 1519 for r in sdata: 1520 r['t'] = r['TimeTag'] - t0 1521 except KeyError: 1522 t0 = (len(sdata)-1)/2 1523 for t,r in enumerate(sdata): 1524 r['t'] = t - t0 1525 1526 1527 def report(self): 1528 ''' 1529 Prints a report on the standardization fit. 1530 Only applicable after `D4xdata.standardize(method='pooled')`. 1531 ''' 1532 report_fit(self.standardization) 1533 1534 1535 def combine_samples(self, sample_groups): 1536 ''' 1537 Combine analyses of different samples to compute weighted average Δ4x 1538 and new error (co)variances corresponding to the groups defined by the `sample_groups` 1539 dictionary. 1540 1541 Caution: samples are weighted by number of replicate analyses, which is a 1542 reasonable default behavior but is not always optimal (e.g., in the case of strongly 1543 correlated analytical errors for one or more samples). 1544 1545 Returns a tuplet of: 1546 1547 + the list of group names 1548 + an array of the corresponding Δ4x values 1549 + the corresponding (co)variance matrix 1550 1551 **Parameters** 1552 1553 + `sample_groups`: a dictionary of the form: 1554 ```py 1555 {'group1': ['sample_1', 'sample_2'], 1556 'group2': ['sample_3', 'sample_4', 'sample_5']} 1557 ``` 1558 ''' 1559 1560 samples = [s for k in sorted(sample_groups.keys()) for s in sorted(sample_groups[k])] 1561 groups = sorted(sample_groups.keys()) 1562 group_total_weights = {k: sum([self.samples[s]['N'] for s in sample_groups[k]]) for k in groups} 1563 D4x_old = np.array([[self.samples[x][f'D{self._4x}']] for x in samples]) 1564 CM_old = np.array([[self.sample_D4x_covar(x,y) for x in samples] for y in samples]) 1565 W = np.array([ 1566 [self.samples[i]['N']/group_total_weights[j] if i in sample_groups[j] else 0 for i in samples] 1567 for j in groups]) 1568 D4x_new = W @ D4x_old 1569 CM_new = W @ CM_old @ W.T 1570 1571 return groups, D4x_new[:,0], CM_new 1572 1573 1574 @make_verbal 1575 def standardize(self, 1576 method = 'pooled', 1577 weighted_sessions = [], 1578 consolidate = True, 1579 consolidate_tables = False, 1580 consolidate_plots = False, 1581 constraints = {}, 1582 ): 1583 ''' 1584 Compute absolute Δ4x values for all replicate analyses and for sample averages. 1585 If `method` argument is set to `'pooled'`, the standardization processes all sessions 1586 in a single step, assuming that all samples (anchors and unknowns alike) are homogeneous, 1587 i.e. that their true Δ4x value does not change between sessions, 1588 ([Daëron, 2021](https://doi.org/10.1029/2020GC009592)). If `method` argument is set to 1589 `'indep_sessions'`, the standardization processes each session independently, based only 1590 on anchors analyses. 1591 ''' 1592 1593 self.standardization_method = method 1594 self.assign_timestamps() 1595 1596 if method == 'pooled': 1597 if weighted_sessions: 1598 for session_group in weighted_sessions: 1599 if self._4x == '47': 1600 X = D47data([r for r in self if r['Session'] in session_group]) 1601 elif self._4x == '48': 1602 X = D48data([r for r in self if r['Session'] in session_group]) 1603 X.Nominal_D4x = self.Nominal_D4x.copy() 1604 X.refresh() 1605 result = X.standardize(method = 'pooled', weighted_sessions = [], consolidate = False) 1606 w = np.sqrt(result.redchi) 1607 self.msg(f'Session group {session_group} MRSWD = {w:.4f}') 1608 for r in X: 1609 r[f'wD{self._4x}raw'] *= w 1610 else: 1611 self.msg(f'All D{self._4x}raw weights set to 1 ‰') 1612 for r in self: 1613 r[f'wD{self._4x}raw'] = 1. 1614 1615 params = Parameters() 1616 for k,session in enumerate(self.sessions): 1617 self.msg(f"Session {session}: scrambling_drift is {self.sessions[session]['scrambling_drift']}.") 1618 self.msg(f"Session {session}: slope_drift is {self.sessions[session]['slope_drift']}.") 1619 self.msg(f"Session {session}: wg_drift is {self.sessions[session]['wg_drift']}.") 1620 s = pf(session) 1621 params.add(f'a_{s}', value = 0.9) 1622 params.add(f'b_{s}', value = 0.) 1623 params.add(f'c_{s}', value = -0.9) 1624 params.add(f'a2_{s}', value = 0., vary = self.sessions[session]['scrambling_drift']) 1625 params.add(f'b2_{s}', value = 0., vary = self.sessions[session]['slope_drift']) 1626 params.add(f'c2_{s}', value = 0., vary = self.sessions[session]['wg_drift']) 1627 for sample in self.unknowns: 1628 params.add(f'D{self._4x}_{pf(sample)}', value = 0.5) 1629 1630 for k in constraints: 1631 params[k].expr = constraints[k] 1632 1633 def residuals(p): 1634 R = [] 1635 for r in self: 1636 session = pf(r['Session']) 1637 sample = pf(r['Sample']) 1638 if r['Sample'] in self.Nominal_D4x: 1639 R += [ ( 1640 r[f'D{self._4x}raw'] - ( 1641 p[f'a_{session}'] * self.Nominal_D4x[r['Sample']] 1642 + p[f'b_{session}'] * r[f'd{self._4x}'] 1643 + p[f'c_{session}'] 1644 + r['t'] * ( 1645 p[f'a2_{session}'] * self.Nominal_D4x[r['Sample']] 1646 + p[f'b2_{session}'] * r[f'd{self._4x}'] 1647 + p[f'c2_{session}'] 1648 ) 1649 ) 1650 ) / r[f'wD{self._4x}raw'] ] 1651 else: 1652 R += [ ( 1653 r[f'D{self._4x}raw'] - ( 1654 p[f'a_{session}'] * p[f'D{self._4x}_{sample}'] 1655 + p[f'b_{session}'] * r[f'd{self._4x}'] 1656 + p[f'c_{session}'] 1657 + r['t'] * ( 1658 p[f'a2_{session}'] * p[f'D{self._4x}_{sample}'] 1659 + p[f'b2_{session}'] * r[f'd{self._4x}'] 1660 + p[f'c2_{session}'] 1661 ) 1662 ) 1663 ) / r[f'wD{self._4x}raw'] ] 1664 return R 1665 1666 M = Minimizer(residuals, params) 1667 result = M.least_squares() 1668 self.Nf = result.nfree 1669 self.t95 = tstudent.ppf(1 - 0.05/2, self.Nf) 1670# if self.verbose: 1671# report_fit(result) 1672 1673 for r in self: 1674 s = pf(r["Session"]) 1675 a = result.params.valuesdict()[f'a_{s}'] 1676 b = result.params.valuesdict()[f'b_{s}'] 1677 c = result.params.valuesdict()[f'c_{s}'] 1678 a2 = result.params.valuesdict()[f'a2_{s}'] 1679 b2 = result.params.valuesdict()[f'b2_{s}'] 1680 c2 = result.params.valuesdict()[f'c2_{s}'] 1681 r[f'D{self._4x}'] = (r[f'D{self._4x}raw'] - c - b * r[f'd{self._4x}'] - c2 * r['t'] - b2 * r['t'] * r[f'd{self._4x}']) / (a + a2 * r['t']) 1682 1683 self.standardization = result 1684 1685 for session in self.sessions: 1686 self.sessions[session]['Np'] = 3 1687 for k in ['scrambling', 'slope', 'wg']: 1688 if self.sessions[session][f'{k}_drift']: 1689 self.sessions[session]['Np'] += 1 1690 1691 if consolidate: 1692 self.consolidate(tables = consolidate_tables, plots = consolidate_plots) 1693 return result 1694 1695 1696 elif method == 'indep_sessions': 1697 1698 if weighted_sessions: 1699 for session_group in weighted_sessions: 1700 X = D4xdata([r for r in self if r['Session'] in session_group], mass = self._4x) 1701 X.Nominal_D4x = self.Nominal_D4x.copy() 1702 X.refresh() 1703 # This is only done to assign r['wD47raw'] for r in X: 1704 X.standardize(method = method, weighted_sessions = [], consolidate = False) 1705 self.msg(f'D{self._4x}raw weights set to {1000*X[0][f"wD{self._4x}raw"]:.1f} ppm for sessions in {session_group}') 1706 else: 1707 self.msg('All weights set to 1 ‰') 1708 for r in self: 1709 r[f'wD{self._4x}raw'] = 1 1710 1711 for session in self.sessions: 1712 s = self.sessions[session] 1713 p_names = ['a', 'b', 'c', 'a2', 'b2', 'c2'] 1714 p_active = [True, True, True, s['scrambling_drift'], s['slope_drift'], s['wg_drift']] 1715 s['Np'] = sum(p_active) 1716 sdata = s['data'] 1717 1718 A = np.array([ 1719 [ 1720 self.Nominal_D4x[r['Sample']] / r[f'wD{self._4x}raw'], 1721 r[f'd{self._4x}'] / r[f'wD{self._4x}raw'], 1722 1 / r[f'wD{self._4x}raw'], 1723 self.Nominal_D4x[r['Sample']] * r['t'] / r[f'wD{self._4x}raw'], 1724 r[f'd{self._4x}'] * r['t'] / r[f'wD{self._4x}raw'], 1725 r['t'] / r[f'wD{self._4x}raw'] 1726 ] 1727 for r in sdata if r['Sample'] in self.anchors 1728 ])[:,p_active] # only keep columns for the active parameters 1729 Y = np.array([[r[f'D{self._4x}raw'] / r[f'wD{self._4x}raw']] for r in sdata if r['Sample'] in self.anchors]) 1730 s['Na'] = Y.size 1731 CM = linalg.inv(A.T @ A) 1732 bf = (CM @ A.T @ Y).T[0,:] 1733 k = 0 1734 for n,a in zip(p_names, p_active): 1735 if a: 1736 s[n] = bf[k] 1737# self.msg(f'{n} = {bf[k]}') 1738 k += 1 1739 else: 1740 s[n] = 0. 1741# self.msg(f'{n} = 0.0') 1742 1743 for r in sdata : 1744 a, b, c, a2, b2, c2 = s['a'], s['b'], s['c'], s['a2'], s['b2'], s['c2'] 1745 r[f'D{self._4x}'] = (r[f'D{self._4x}raw'] - c - b * r[f'd{self._4x}'] - c2 * r['t'] - b2 * r['t'] * r[f'd{self._4x}']) / (a + a2 * r['t']) 1746 r[f'wD{self._4x}'] = r[f'wD{self._4x}raw'] / (a + a2 * r['t']) 1747 1748 s['CM'] = np.zeros((6,6)) 1749 i = 0 1750 k_active = [j for j,a in enumerate(p_active) if a] 1751 for j,a in enumerate(p_active): 1752 if a: 1753 s['CM'][j,k_active] = CM[i,:] 1754 i += 1 1755 1756 if not weighted_sessions: 1757 w = self.rmswd()['rmswd'] 1758 for r in self: 1759 r[f'wD{self._4x}'] *= w 1760 r[f'wD{self._4x}raw'] *= w 1761 for session in self.sessions: 1762 self.sessions[session]['CM'] *= w**2 1763 1764 for session in self.sessions: 1765 s = self.sessions[session] 1766 s['SE_a'] = s['CM'][0,0]**.5 1767 s['SE_b'] = s['CM'][1,1]**.5 1768 s['SE_c'] = s['CM'][2,2]**.5 1769 s['SE_a2'] = s['CM'][3,3]**.5 1770 s['SE_b2'] = s['CM'][4,4]**.5 1771 s['SE_c2'] = s['CM'][5,5]**.5 1772 1773 if not weighted_sessions: 1774 self.Nf = len(self) - len(self.unknowns) - np.sum([self.sessions[s]['Np'] for s in self.sessions]) 1775 else: 1776 self.Nf = 0 1777 for sg in weighted_sessions: 1778 self.Nf += self.rmswd(sessions = sg)['Nf'] 1779 1780 self.t95 = tstudent.ppf(1 - 0.05/2, self.Nf) 1781 1782 avgD4x = { 1783 sample: np.mean([r[f'D{self._4x}'] for r in self if r['Sample'] == sample]) 1784 for sample in self.samples 1785 } 1786 chi2 = np.sum([(r[f'D{self._4x}'] - avgD4x[r['Sample']])**2 for r in self]) 1787 rD4x = (chi2/self.Nf)**.5 1788 self.repeatability[f'sigma_{self._4x}'] = rD4x 1789 1790 if consolidate: 1791 self.consolidate(tables = consolidate_tables, plots = consolidate_plots) 1792 1793 1794 def standardization_error(self, session, d4x, D4x, t = 0): 1795 ''' 1796 Compute standardization error for a given session and 1797 (δ47, Δ47) composition. 1798 ''' 1799 a = self.sessions[session]['a'] 1800 b = self.sessions[session]['b'] 1801 c = self.sessions[session]['c'] 1802 a2 = self.sessions[session]['a2'] 1803 b2 = self.sessions[session]['b2'] 1804 c2 = self.sessions[session]['c2'] 1805 CM = self.sessions[session]['CM'] 1806 1807 x, y = D4x, d4x 1808 z = a * x + b * y + c + a2 * x * t + b2 * y * t + c2 * t 1809# x = (z - b*y - b2*y*t - c - c2*t) / (a+a2*t) 1810 dxdy = -(b+b2*t) / (a+a2*t) 1811 dxdz = 1. / (a+a2*t) 1812 dxda = -x / (a+a2*t) 1813 dxdb = -y / (a+a2*t) 1814 dxdc = -1. / (a+a2*t) 1815 dxda2 = -x * a2 / (a+a2*t) 1816 dxdb2 = -y * t / (a+a2*t) 1817 dxdc2 = -t / (a+a2*t) 1818 V = np.array([dxda, dxdb, dxdc, dxda2, dxdb2, dxdc2]) 1819 sx = (V @ CM @ V.T) ** .5 1820 return sx 1821 1822 1823 @make_verbal 1824 def summary(self, 1825 dir = 'output', 1826 filename = None, 1827 save_to_file = True, 1828 print_out = True, 1829 ): 1830 ''' 1831 Print out an/or save to disk a summary of the standardization results. 1832 1833 **Parameters** 1834 1835 + `dir`: the directory in which to save the table 1836 + `filename`: the name to the csv file to write to 1837 + `save_to_file`: whether to save the table to disk 1838 + `print_out`: whether to print out the table 1839 ''' 1840 1841 out = [] 1842 out += [['N samples (anchors + unknowns)', f"{len(self.samples)} ({len(self.anchors)} + {len(self.unknowns)})"]] 1843 out += [['N analyses (anchors + unknowns)', f"{len(self)} ({len([r for r in self if r['Sample'] in self.anchors])} + {len([r for r in self if r['Sample'] in self.unknowns])})"]] 1844 out += [['Repeatability of δ13C_VPDB', f"{1000 * self.repeatability['r_d13C_VPDB']:.1f} ppm"]] 1845 out += [['Repeatability of δ18O_VSMOW', f"{1000 * self.repeatability['r_d18O_VSMOW']:.1f} ppm"]] 1846 out += [[f'Repeatability of Δ{self._4x} (anchors)', f"{1000 * self.repeatability[f'r_D{self._4x}a']:.1f} ppm"]] 1847 out += [[f'Repeatability of Δ{self._4x} (unknowns)', f"{1000 * self.repeatability[f'r_D{self._4x}u']:.1f} ppm"]] 1848 out += [[f'Repeatability of Δ{self._4x} (all)', f"{1000 * self.repeatability[f'r_D{self._4x}']:.1f} ppm"]] 1849 out += [['Model degrees of freedom', f"{self.Nf}"]] 1850 out += [['Student\'s 95% t-factor', f"{self.t95:.2f}"]] 1851 out += [['Standardization method', self.standardization_method]] 1852 1853 if save_to_file: 1854 if not os.path.exists(dir): 1855 os.makedirs(dir) 1856 if filename is None: 1857 filename = f'D{self._4x}_summary.csv' 1858 with open(f'{dir}/{filename}', 'w') as fid: 1859 fid.write(make_csv(out)) 1860 if print_out: 1861 self.msg('\n' + pretty_table(out, header = 0)) 1862 1863 1864 @make_verbal 1865 def table_of_sessions(self, 1866 dir = 'output', 1867 filename = None, 1868 save_to_file = True, 1869 print_out = True, 1870 output = None, 1871 ): 1872 ''' 1873 Print out an/or save to disk a table of sessions. 1874 1875 **Parameters** 1876 1877 + `dir`: the directory in which to save the table 1878 + `filename`: the name to the csv file to write to 1879 + `save_to_file`: whether to save the table to disk 1880 + `print_out`: whether to print out the table 1881 + `output`: if set to `'pretty'`: return a pretty text table (see `pretty_table()`); 1882 if set to `'raw'`: return a list of list of strings 1883 (e.g., `[['header1', 'header2'], ['0.1', '0.2']]`) 1884 ''' 1885 include_a2 = any([self.sessions[session]['scrambling_drift'] for session in self.sessions]) 1886 include_b2 = any([self.sessions[session]['slope_drift'] for session in self.sessions]) 1887 include_c2 = any([self.sessions[session]['wg_drift'] for session in self.sessions]) 1888 1889 out = [['Session','Na','Nu','d13Cwg_VPDB','d18Owg_VSMOW','r_d13C','r_d18O',f'r_D{self._4x}','a ± SE','1e3 x b ± SE','c ± SE']] 1890 if include_a2: 1891 out[-1] += ['a2 ± SE'] 1892 if include_b2: 1893 out[-1] += ['b2 ± SE'] 1894 if include_c2: 1895 out[-1] += ['c2 ± SE'] 1896 for session in self.sessions: 1897 out += [[ 1898 session, 1899 f"{self.sessions[session]['Na']}", 1900 f"{self.sessions[session]['Nu']}", 1901 f"{self.sessions[session]['d13Cwg_VPDB']:.3f}", 1902 f"{self.sessions[session]['d18Owg_VSMOW']:.3f}", 1903 f"{self.sessions[session]['r_d13C_VPDB']:.4f}", 1904 f"{self.sessions[session]['r_d18O_VSMOW']:.4f}", 1905 f"{self.sessions[session][f'r_D{self._4x}']:.4f}", 1906 f"{self.sessions[session]['a']:.3f} ± {self.sessions[session]['SE_a']:.3f}", 1907 f"{1e3*self.sessions[session]['b']:.3f} ± {1e3*self.sessions[session]['SE_b']:.3f}", 1908 f"{self.sessions[session]['c']:.3f} ± {self.sessions[session]['SE_c']:.3f}", 1909 ]] 1910 if include_a2: 1911 if self.sessions[session]['scrambling_drift']: 1912 out[-1] += [f"{self.sessions[session]['a2']:.1e} ± {self.sessions[session]['SE_a2']:.1e}"] 1913 else: 1914 out[-1] += [''] 1915 if include_b2: 1916 if self.sessions[session]['slope_drift']: 1917 out[-1] += [f"{self.sessions[session]['b2']:.1e} ± {self.sessions[session]['SE_b2']:.1e}"] 1918 else: 1919 out[-1] += [''] 1920 if include_c2: 1921 if self.sessions[session]['wg_drift']: 1922 out[-1] += [f"{self.sessions[session]['c2']:.1e} ± {self.sessions[session]['SE_c2']:.1e}"] 1923 else: 1924 out[-1] += [''] 1925 1926 if save_to_file: 1927 if not os.path.exists(dir): 1928 os.makedirs(dir) 1929 if filename is None: 1930 filename = f'D{self._4x}_sessions.csv' 1931 with open(f'{dir}/{filename}', 'w') as fid: 1932 fid.write(make_csv(out)) 1933 if print_out: 1934 self.msg('\n' + pretty_table(out)) 1935 if output == 'raw': 1936 return out 1937 elif output == 'pretty': 1938 return pretty_table(out) 1939 1940 1941 @make_verbal 1942 def table_of_analyses( 1943 self, 1944 dir = 'output', 1945 filename = None, 1946 save_to_file = True, 1947 print_out = True, 1948 output = None, 1949 ): 1950 ''' 1951 Print out an/or save to disk a table of analyses. 1952 1953 **Parameters** 1954 1955 + `dir`: the directory in which to save the table 1956 + `filename`: the name to the csv file to write to 1957 + `save_to_file`: whether to save the table to disk 1958 + `print_out`: whether to print out the table 1959 + `output`: if set to `'pretty'`: return a pretty text table (see `pretty_table()`); 1960 if set to `'raw'`: return a list of list of strings 1961 (e.g., `[['header1', 'header2'], ['0.1', '0.2']]`) 1962 ''' 1963 1964 out = [['UID','Session','Sample']] 1965 extra_fields = [f for f in [('SampleMass','.2f'),('ColdFingerPressure','.1f'),('AcidReactionYield','.3f')] if f[0] in {k for r in self for k in r}] 1966 for f in extra_fields: 1967 out[-1] += [f[0]] 1968 out[-1] += ['d13Cwg_VPDB','d18Owg_VSMOW','d45','d46','d47','d48','d49','d13C_VPDB','d18O_VSMOW','D47raw','D48raw','D49raw',f'D{self._4x}'] 1969 for r in self: 1970 out += [[f"{r['UID']}",f"{r['Session']}",f"{r['Sample']}"]] 1971 for f in extra_fields: 1972 out[-1] += [f"{r[f[0]]:{f[1]}}"] 1973 out[-1] += [ 1974 f"{r['d13Cwg_VPDB']:.3f}", 1975 f"{r['d18Owg_VSMOW']:.3f}", 1976 f"{r['d45']:.6f}", 1977 f"{r['d46']:.6f}", 1978 f"{r['d47']:.6f}", 1979 f"{r['d48']:.6f}", 1980 f"{r['d49']:.6f}", 1981 f"{r['d13C_VPDB']:.6f}", 1982 f"{r['d18O_VSMOW']:.6f}", 1983 f"{r['D47raw']:.6f}", 1984 f"{r['D48raw']:.6f}", 1985 f"{r['D49raw']:.6f}", 1986 f"{r[f'D{self._4x}']:.6f}" 1987 ] 1988 if save_to_file: 1989 if not os.path.exists(dir): 1990 os.makedirs(dir) 1991 if filename is None: 1992 filename = f'D{self._4x}_analyses.csv' 1993 with open(f'{dir}/{filename}', 'w') as fid: 1994 fid.write(make_csv(out)) 1995 if print_out: 1996 self.msg('\n' + pretty_table(out)) 1997 return out 1998 1999 @make_verbal 2000 def covar_table( 2001 self, 2002 correl = False, 2003 dir = 'output', 2004 filename = None, 2005 save_to_file = True, 2006 print_out = True, 2007 output = None, 2008 ): 2009 ''' 2010 Print out, save to disk and/or return the variance-covariance matrix of D4x 2011 for all unknown samples. 2012 2013 **Parameters** 2014 2015 + `dir`: the directory in which to save the csv 2016 + `filename`: the name of the csv file to write to 2017 + `save_to_file`: whether to save the csv 2018 + `print_out`: whether to print out the matrix 2019 + `output`: if set to `'pretty'`: return a pretty text matrix (see `pretty_table()`); 2020 if set to `'raw'`: return a list of list of strings 2021 (e.g., `[['header1', 'header2'], ['0.1', '0.2']]`) 2022 ''' 2023 samples = sorted([u for u in self.unknowns]) 2024 out = [[''] + samples] 2025 for s1 in samples: 2026 out.append([s1]) 2027 for s2 in samples: 2028 if correl: 2029 out[-1].append(f'{self.sample_D4x_correl(s1, s2):.6f}') 2030 else: 2031 out[-1].append(f'{self.sample_D4x_covar(s1, s2):.8e}') 2032 2033 if save_to_file: 2034 if not os.path.exists(dir): 2035 os.makedirs(dir) 2036 if filename is None: 2037 if correl: 2038 filename = f'D{self._4x}_correl.csv' 2039 else: 2040 filename = f'D{self._4x}_covar.csv' 2041 with open(f'{dir}/{filename}', 'w') as fid: 2042 fid.write(make_csv(out)) 2043 if print_out: 2044 self.msg('\n'+pretty_table(out)) 2045 if output == 'raw': 2046 return out 2047 elif output == 'pretty': 2048 return pretty_table(out) 2049 2050 @make_verbal 2051 def table_of_samples( 2052 self, 2053 dir = 'output', 2054 filename = None, 2055 save_to_file = True, 2056 print_out = True, 2057 output = None, 2058 ): 2059 ''' 2060 Print out, save to disk and/or return a table of samples. 2061 2062 **Parameters** 2063 2064 + `dir`: the directory in which to save the csv 2065 + `filename`: the name of the csv file to write to 2066 + `save_to_file`: whether to save the csv 2067 + `print_out`: whether to print out the table 2068 + `output`: if set to `'pretty'`: return a pretty text table (see `pretty_table()`); 2069 if set to `'raw'`: return a list of list of strings 2070 (e.g., `[['header1', 'header2'], ['0.1', '0.2']]`) 2071 ''' 2072 2073 out = [['Sample','N','d13C_VPDB','d18O_VSMOW',f'D{self._4x}','SE','95% CL','SD','p_Levene']] 2074 for sample in self.anchors: 2075 out += [[ 2076 f"{sample}", 2077 f"{self.samples[sample]['N']}", 2078 f"{self.samples[sample]['d13C_VPDB']:.2f}", 2079 f"{self.samples[sample]['d18O_VSMOW']:.2f}", 2080 f"{self.samples[sample][f'D{self._4x}']:.4f}",'','', 2081 f"{self.samples[sample][f'SD_D{self._4x}']:.4f}" if self.samples[sample]['N'] > 1 else '', '' 2082 ]] 2083 for sample in self.unknowns: 2084 out += [[ 2085 f"{sample}", 2086 f"{self.samples[sample]['N']}", 2087 f"{self.samples[sample]['d13C_VPDB']:.2f}", 2088 f"{self.samples[sample]['d18O_VSMOW']:.2f}", 2089 f"{self.samples[sample][f'D{self._4x}']:.4f}", 2090 f"{self.samples[sample][f'SE_D{self._4x}']:.4f}", 2091 f"± {self.samples[sample][f'SE_D{self._4x}'] * self.t95:.4f}", 2092 f"{self.samples[sample][f'SD_D{self._4x}']:.4f}" if self.samples[sample]['N'] > 1 else '', 2093 f"{self.samples[sample]['p_Levene']:.3f}" if self.samples[sample]['N'] > 2 else '' 2094 ]] 2095 if save_to_file: 2096 if not os.path.exists(dir): 2097 os.makedirs(dir) 2098 if filename is None: 2099 filename = f'D{self._4x}_samples.csv' 2100 with open(f'{dir}/{filename}', 'w') as fid: 2101 fid.write(make_csv(out)) 2102 if print_out: 2103 self.msg('\n'+pretty_table(out)) 2104 if output == 'raw': 2105 return out 2106 elif output == 'pretty': 2107 return pretty_table(out) 2108 2109 2110 def plot_sessions(self, dir = 'output', figsize = (8,8)): 2111 ''' 2112 Generate session plots and save them to disk. 2113 2114 **Parameters** 2115 2116 + `dir`: the directory in which to save the plots 2117 + `figsize`: the width and height (in inches) of each plot 2118 ''' 2119 if not os.path.exists(dir): 2120 os.makedirs(dir) 2121 2122 for session in self.sessions: 2123 sp = self.plot_single_session(session, xylimits = 'constant') 2124 ppl.savefig(f'{dir}/D{self._4x}_plot_{session}.pdf') 2125 ppl.close(sp.fig) 2126 2127 2128 @make_verbal 2129 def consolidate_samples(self): 2130 ''' 2131 Compile various statistics for each sample. 2132 2133 For each anchor sample: 2134 2135 + `D47` or `D48`: the nominal Δ4x value for this anchor, specified by `self.Nominal_D4x` 2136 + `SE_D47` or `SE_D48`: set to zero by definition 2137 2138 For each unknown sample: 2139 2140 + `D47` or `D48`: the standardized Δ4x value for this unknown 2141 + `SE_D47` or `SE_D48`: the standard error of Δ4x for this unknown 2142 2143 For each anchor and unknown: 2144 2145 + `N`: the total number of analyses of this sample 2146 + `SD_D47` or `SD_D48`: the “sample” (in the statistical sense) standard deviation for this sample 2147 + `d13C_VPDB`: the average δ13C_VPDB value for this sample 2148 + `d18O_VSMOW`: the average δ18O_VSMOW value for this sample (as CO2) 2149 + `p_Levene`: the p-value from a [Levene test](https://en.wikipedia.org/wiki/Levene%27s_test) of equal 2150 variance, indicating whether the Δ4x repeatability this sample differs significantly from 2151 that observed for the reference sample specified by `self.LEVENE_REF_SAMPLE`. 2152 ''' 2153 D4x_ref_pop = [r[f'D{self._4x}'] for r in self.samples[self.LEVENE_REF_SAMPLE]['data']] 2154 for sample in self.samples: 2155 self.samples[sample]['N'] = len(self.samples[sample]['data']) 2156 if self.samples[sample]['N'] > 1: 2157 self.samples[sample][f'SD_D{self._4x}'] = stdev([r[f'D{self._4x}'] for r in self.samples[sample]['data']]) 2158 2159 self.samples[sample]['d13C_VPDB'] = np.mean([r['d13C_VPDB'] for r in self.samples[sample]['data']]) 2160 self.samples[sample]['d18O_VSMOW'] = np.mean([r['d18O_VSMOW'] for r in self.samples[sample]['data']]) 2161 2162 D4x_pop = [r[f'D{self._4x}'] for r in self.samples[sample]['data']] 2163 if len(D4x_pop) > 2: 2164 self.samples[sample]['p_Levene'] = levene(D4x_ref_pop, D4x_pop, center = 'median')[1] 2165 2166 if self.standardization_method == 'pooled': 2167 for sample in self.anchors: 2168 self.samples[sample][f'D{self._4x}'] = self.Nominal_D4x[sample] 2169 self.samples[sample][f'SE_D{self._4x}'] = 0. 2170 for sample in self.unknowns: 2171 self.samples[sample][f'D{self._4x}'] = self.standardization.params.valuesdict()[f'D{self._4x}_{pf(sample)}'] 2172 try: 2173 self.samples[sample][f'SE_D{self._4x}'] = self.sample_D4x_covar(sample)**.5 2174 except ValueError: 2175 # when `sample` is constrained by self.standardize(constraints = {...}), 2176 # it is no longer listed in self.standardization.var_names. 2177 # Temporary fix: define SE as zero for now 2178 self.samples[sample][f'SE_D4{self._4x}'] = 0. 2179 2180 elif self.standardization_method == 'indep_sessions': 2181 for sample in self.anchors: 2182 self.samples[sample][f'D{self._4x}'] = self.Nominal_D4x[sample] 2183 self.samples[sample][f'SE_D{self._4x}'] = 0. 2184 for sample in self.unknowns: 2185 self.msg(f'Consolidating sample {sample}') 2186 self.unknowns[sample][f'session_D{self._4x}'] = {} 2187 session_avg = [] 2188 for session in self.sessions: 2189 sdata = [r for r in self.sessions[session]['data'] if r['Sample'] == sample] 2190 if sdata: 2191 self.msg(f'{sample} found in session {session}') 2192 avg_D4x = np.mean([r[f'D{self._4x}'] for r in sdata]) 2193 avg_d4x = np.mean([r[f'd{self._4x}'] for r in sdata]) 2194 # !! TODO: sigma_s below does not account for temporal changes in standardization error 2195 sigma_s = self.standardization_error(session, avg_d4x, avg_D4x) 2196 sigma_u = sdata[0][f'wD{self._4x}raw'] / self.sessions[session]['a'] / len(sdata)**.5 2197 session_avg.append([avg_D4x, (sigma_u**2 + sigma_s**2)**.5]) 2198 self.unknowns[sample][f'session_D{self._4x}'][session] = session_avg[-1] 2199 self.samples[sample][f'D{self._4x}'], self.samples[sample][f'SE_D{self._4x}'] = w_avg(*zip(*session_avg)) 2200 weights = {s: self.unknowns[sample][f'session_D{self._4x}'][s][1]**-2 for s in self.unknowns[sample][f'session_D{self._4x}']} 2201 wsum = sum([weights[s] for s in weights]) 2202 for s in weights: 2203 self.unknowns[sample][f'session_D{self._4x}'][s] += [self.unknowns[sample][f'session_D{self._4x}'][s][1]**-2 / wsum] 2204 2205 2206 def consolidate_sessions(self): 2207 ''' 2208 Compute various statistics for each session. 2209 2210 + `Na`: Number of anchor analyses in the session 2211 + `Nu`: Number of unknown analyses in the session 2212 + `r_d13C_VPDB`: δ13C_VPDB repeatability of analyses within the session 2213 + `r_d18O_VSMOW`: δ18O_VSMOW repeatability of analyses within the session 2214 + `r_D47` or `r_D48`: Δ4x repeatability of analyses within the session 2215 + `a`: scrambling factor 2216 + `b`: compositional slope 2217 + `c`: WG offset 2218 + `SE_a`: Model stadard erorr of `a` 2219 + `SE_b`: Model stadard erorr of `b` 2220 + `SE_c`: Model stadard erorr of `c` 2221 + `scrambling_drift` (boolean): whether to allow a temporal drift in the scrambling factor (`a`) 2222 + `slope_drift` (boolean): whether to allow a temporal drift in the compositional slope (`b`) 2223 + `wg_drift` (boolean): whether to allow a temporal drift in the WG offset (`c`) 2224 + `a2`: scrambling factor drift 2225 + `b2`: compositional slope drift 2226 + `c2`: WG offset drift 2227 + `Np`: Number of standardization parameters to fit 2228 + `CM`: model covariance matrix for (`a`, `b`, `c`, `a2`, `b2`, `c2`) 2229 + `d13Cwg_VPDB`: δ13C_VPDB of WG 2230 + `d18Owg_VSMOW`: δ18O_VSMOW of WG 2231 ''' 2232 for session in self.sessions: 2233 if 'd13Cwg_VPDB' not in self.sessions[session]: 2234 self.sessions[session]['d13Cwg_VPDB'] = self.sessions[session]['data'][0]['d13Cwg_VPDB'] 2235 if 'd18Owg_VSMOW' not in self.sessions[session]: 2236 self.sessions[session]['d18Owg_VSMOW'] = self.sessions[session]['data'][0]['d18Owg_VSMOW'] 2237 self.sessions[session]['Na'] = len([r for r in self.sessions[session]['data'] if r['Sample'] in self.anchors]) 2238 self.sessions[session]['Nu'] = len([r for r in self.sessions[session]['data'] if r['Sample'] in self.unknowns]) 2239 2240 self.msg(f'Computing repeatabilities for session {session}') 2241 self.sessions[session]['r_d13C_VPDB'] = self.compute_r('d13C_VPDB', samples = 'anchors', sessions = [session]) 2242 self.sessions[session]['r_d18O_VSMOW'] = self.compute_r('d18O_VSMOW', samples = 'anchors', sessions = [session]) 2243 self.sessions[session][f'r_D{self._4x}'] = self.compute_r(f'D{self._4x}', sessions = [session]) 2244 2245 if self.standardization_method == 'pooled': 2246 for session in self.sessions: 2247 2248 self.sessions[session]['a'] = self.standardization.params.valuesdict()[f'a_{pf(session)}'] 2249 i = self.standardization.var_names.index(f'a_{pf(session)}') 2250 self.sessions[session]['SE_a'] = self.standardization.covar[i,i]**.5 2251 2252 self.sessions[session]['b'] = self.standardization.params.valuesdict()[f'b_{pf(session)}'] 2253 i = self.standardization.var_names.index(f'b_{pf(session)}') 2254 self.sessions[session]['SE_b'] = self.standardization.covar[i,i]**.5 2255 2256 self.sessions[session]['c'] = self.standardization.params.valuesdict()[f'c_{pf(session)}'] 2257 i = self.standardization.var_names.index(f'c_{pf(session)}') 2258 self.sessions[session]['SE_c'] = self.standardization.covar[i,i]**.5 2259 2260 self.sessions[session]['a2'] = self.standardization.params.valuesdict()[f'a2_{pf(session)}'] 2261 if self.sessions[session]['scrambling_drift']: 2262 i = self.standardization.var_names.index(f'a2_{pf(session)}') 2263 self.sessions[session]['SE_a2'] = self.standardization.covar[i,i]**.5 2264 else: 2265 self.sessions[session]['SE_a2'] = 0. 2266 2267 self.sessions[session]['b2'] = self.standardization.params.valuesdict()[f'b2_{pf(session)}'] 2268 if self.sessions[session]['slope_drift']: 2269 i = self.standardization.var_names.index(f'b2_{pf(session)}') 2270 self.sessions[session]['SE_b2'] = self.standardization.covar[i,i]**.5 2271 else: 2272 self.sessions[session]['SE_b2'] = 0. 2273 2274 self.sessions[session]['c2'] = self.standardization.params.valuesdict()[f'c2_{pf(session)}'] 2275 if self.sessions[session]['wg_drift']: 2276 i = self.standardization.var_names.index(f'c2_{pf(session)}') 2277 self.sessions[session]['SE_c2'] = self.standardization.covar[i,i]**.5 2278 else: 2279 self.sessions[session]['SE_c2'] = 0. 2280 2281 i = self.standardization.var_names.index(f'a_{pf(session)}') 2282 j = self.standardization.var_names.index(f'b_{pf(session)}') 2283 k = self.standardization.var_names.index(f'c_{pf(session)}') 2284 CM = np.zeros((6,6)) 2285 CM[:3,:3] = self.standardization.covar[[i,j,k],:][:,[i,j,k]] 2286 try: 2287 i2 = self.standardization.var_names.index(f'a2_{pf(session)}') 2288 CM[3,[0,1,2,3]] = self.standardization.covar[i2,[i,j,k,i2]] 2289 CM[[0,1,2,3],3] = self.standardization.covar[[i,j,k,i2],i2] 2290 try: 2291 j2 = self.standardization.var_names.index(f'b2_{pf(session)}') 2292 CM[3,4] = self.standardization.covar[i2,j2] 2293 CM[4,3] = self.standardization.covar[j2,i2] 2294 except ValueError: 2295 pass 2296 try: 2297 k2 = self.standardization.var_names.index(f'c2_{pf(session)}') 2298 CM[3,5] = self.standardization.covar[i2,k2] 2299 CM[5,3] = self.standardization.covar[k2,i2] 2300 except ValueError: 2301 pass 2302 except ValueError: 2303 pass 2304 try: 2305 j2 = self.standardization.var_names.index(f'b2_{pf(session)}') 2306 CM[4,[0,1,2,4]] = self.standardization.covar[j2,[i,j,k,j2]] 2307 CM[[0,1,2,4],4] = self.standardization.covar[[i,j,k,j2],j2] 2308 try: 2309 k2 = self.standardization.var_names.index(f'c2_{pf(session)}') 2310 CM[4,5] = self.standardization.covar[j2,k2] 2311 CM[5,4] = self.standardization.covar[k2,j2] 2312 except ValueError: 2313 pass 2314 except ValueError: 2315 pass 2316 try: 2317 k2 = self.standardization.var_names.index(f'c2_{pf(session)}') 2318 CM[5,[0,1,2,5]] = self.standardization.covar[k2,[i,j,k,k2]] 2319 CM[[0,1,2,5],5] = self.standardization.covar[[i,j,k,k2],k2] 2320 except ValueError: 2321 pass 2322 2323 self.sessions[session]['CM'] = CM 2324 2325 elif self.standardization_method == 'indep_sessions': 2326 pass # Not implemented yet 2327 2328 2329 @make_verbal 2330 def repeatabilities(self): 2331 ''' 2332 Compute analytical repeatabilities for δ13C_VPDB, δ18O_VSMOW, Δ4x 2333 (for all samples, for anchors, and for unknowns). 2334 ''' 2335 self.msg('Computing reproducibilities for all sessions') 2336 2337 self.repeatability['r_d13C_VPDB'] = self.compute_r('d13C_VPDB', samples = 'anchors') 2338 self.repeatability['r_d18O_VSMOW'] = self.compute_r('d18O_VSMOW', samples = 'anchors') 2339 self.repeatability[f'r_D{self._4x}a'] = self.compute_r(f'D{self._4x}', samples = 'anchors') 2340 self.repeatability[f'r_D{self._4x}u'] = self.compute_r(f'D{self._4x}', samples = 'unknowns') 2341 self.repeatability[f'r_D{self._4x}'] = self.compute_r(f'D{self._4x}', samples = 'all samples') 2342 2343 2344 @make_verbal 2345 def consolidate(self, tables = True, plots = True): 2346 ''' 2347 Collect information about samples, sessions and repeatabilities. 2348 ''' 2349 self.consolidate_samples() 2350 self.consolidate_sessions() 2351 self.repeatabilities() 2352 2353 if tables: 2354 self.summary() 2355 self.table_of_sessions() 2356 self.table_of_analyses() 2357 self.table_of_samples() 2358 2359 if plots: 2360 self.plot_sessions() 2361 2362 2363 @make_verbal 2364 def rmswd(self, 2365 samples = 'all samples', 2366 sessions = 'all sessions', 2367 ): 2368 ''' 2369 Compute the χ2, root mean squared weighted deviation 2370 (i.e. reduced χ2), and corresponding degrees of freedom of the 2371 Δ4x values for samples in `samples` and sessions in `sessions`. 2372 2373 Only used in `D4xdata.standardize()` with `method='indep_sessions'`. 2374 ''' 2375 if samples == 'all samples': 2376 mysamples = [k for k in self.samples] 2377 elif samples == 'anchors': 2378 mysamples = [k for k in self.anchors] 2379 elif samples == 'unknowns': 2380 mysamples = [k for k in self.unknowns] 2381 else: 2382 mysamples = samples 2383 2384 if sessions == 'all sessions': 2385 sessions = [k for k in self.sessions] 2386 2387 chisq, Nf = 0, 0 2388 for sample in mysamples : 2389 G = [ r for r in self if r['Sample'] == sample and r['Session'] in sessions ] 2390 if len(G) > 1 : 2391 X, sX = w_avg([r[f'D{self._4x}'] for r in G], [r[f'wD{self._4x}'] for r in G]) 2392 Nf += (len(G) - 1) 2393 chisq += np.sum([ ((r[f'D{self._4x}']-X)/r[f'wD{self._4x}'])**2 for r in G]) 2394 r = (chisq / Nf)**.5 if Nf > 0 else 0 2395 self.msg(f'RMSWD of r["D{self._4x}"] is {r:.6f} for {samples}.') 2396 return {'rmswd': r, 'chisq': chisq, 'Nf': Nf} 2397 2398 2399 @make_verbal 2400 def compute_r(self, key, samples = 'all samples', sessions = 'all sessions'): 2401 ''' 2402 Compute the repeatability of `[r[key] for r in self]` 2403 ''' 2404 # NB: it's debatable whether rD47 should be computed 2405 # with Nf = len(self)-len(self.samples) instead of 2406 # Nf = len(self) - len(self.unknwons) - 3*len(self.sessions) 2407 2408 if samples == 'all samples': 2409 mysamples = [k for k in self.samples] 2410 elif samples == 'anchors': 2411 mysamples = [k for k in self.anchors] 2412 elif samples == 'unknowns': 2413 mysamples = [k for k in self.unknowns] 2414 else: 2415 mysamples = samples 2416 2417 if sessions == 'all sessions': 2418 sessions = [k for k in self.sessions] 2419 2420 if key in ['D47', 'D48']: 2421 chisq, Nf = 0, 0 2422 for sample in mysamples : 2423 X = [ r[key] for r in self if r['Sample'] == sample and r['Session'] in sessions ] 2424 if len(X) > 1 : 2425 chisq += np.sum([ (x-self.samples[sample][key])**2 for x in X ]) 2426 if sample in self.unknowns: 2427 Nf += len(X) - 1 2428 else: 2429 Nf += len(X) 2430 if samples in ['anchors', 'all samples']: 2431 Nf -= sum([self.sessions[s]['Np'] for s in sessions]) 2432 r = (chisq / Nf)**.5 if Nf > 0 else 0 2433 2434 else: # if key not in ['D47', 'D48'] 2435 chisq, Nf = 0, 0 2436 for sample in mysamples : 2437 X = [ r[key] for r in self if r['Sample'] == sample and r['Session'] in sessions ] 2438 if len(X) > 1 : 2439 Nf += len(X) - 1 2440 chisq += np.sum([ (x-np.mean(X))**2 for x in X ]) 2441 r = (chisq / Nf)**.5 if Nf > 0 else 0 2442 2443 self.msg(f'Repeatability of r["{key}"] is {1000*r:.1f} ppm for {samples}.') 2444 return r 2445 2446 def sample_average(self, samples, weights = 'equal', normalize = True): 2447 ''' 2448 Weighted average Δ4x value of a group of samples, accounting for covariance. 2449 2450 Returns the weighed average Δ4x value and associated SE 2451 of a group of samples. Weights are equal by default. If `normalize` is 2452 true, `weights` will be rescaled so that their sum equals 1. 2453 2454 **Examples** 2455 2456 ```python 2457 self.sample_average(['X','Y'], [1, 2]) 2458 ``` 2459 2460 returns the value and SE of [Δ4x(X) + 2 Δ4x(Y)]/3, 2461 where Δ4x(X) and Δ4x(Y) are the average Δ4x 2462 values of samples X and Y, respectively. 2463 2464 ```python 2465 self.sample_average(['X','Y'], [1, -1], normalize = False) 2466 ``` 2467 2468 returns the value and SE of the difference Δ4x(X) - Δ4x(Y). 2469 ''' 2470 if weights == 'equal': 2471 weights = [1/len(samples)] * len(samples) 2472 2473 if normalize: 2474 s = sum(weights) 2475 if s: 2476 weights = [w/s for w in weights] 2477 2478 try: 2479# indices = [self.standardization.var_names.index(f'D47_{pf(sample)}') for sample in samples] 2480# C = self.standardization.covar[indices,:][:,indices] 2481 C = np.array([[self.sample_D4x_covar(x, y) for x in samples] for y in samples]) 2482 X = [self.samples[sample][f'D{self._4x}'] for sample in samples] 2483 return correlated_sum(X, C, weights) 2484 except ValueError: 2485 return (0., 0.) 2486 2487 2488 def sample_D4x_covar(self, sample1, sample2 = None): 2489 ''' 2490 Covariance between Δ4x values of samples 2491 2492 Returns the error covariance between the average Δ4x values of two 2493 samples. If if only `sample_1` is specified, or if `sample_1 == sample_2`), 2494 returns the Δ4x variance for that sample. 2495 ''' 2496 if sample2 is None: 2497 sample2 = sample1 2498 if self.standardization_method == 'pooled': 2499 i = self.standardization.var_names.index(f'D{self._4x}_{pf(sample1)}') 2500 j = self.standardization.var_names.index(f'D{self._4x}_{pf(sample2)}') 2501 return self.standardization.covar[i, j] 2502 elif self.standardization_method == 'indep_sessions': 2503 if sample1 == sample2: 2504 return self.samples[sample1][f'SE_D{self._4x}']**2 2505 else: 2506 c = 0 2507 for session in self.sessions: 2508 sdata1 = [r for r in self.sessions[session]['data'] if r['Sample'] == sample1] 2509 sdata2 = [r for r in self.sessions[session]['data'] if r['Sample'] == sample2] 2510 if sdata1 and sdata2: 2511 a = self.sessions[session]['a'] 2512 # !! TODO: CM below does not account for temporal changes in standardization parameters 2513 CM = self.sessions[session]['CM'][:3,:3] 2514 avg_D4x_1 = np.mean([r[f'D{self._4x}'] for r in sdata1]) 2515 avg_d4x_1 = np.mean([r[f'd{self._4x}'] for r in sdata1]) 2516 avg_D4x_2 = np.mean([r[f'D{self._4x}'] for r in sdata2]) 2517 avg_d4x_2 = np.mean([r[f'd{self._4x}'] for r in sdata2]) 2518 c += ( 2519 self.unknowns[sample1][f'session_D{self._4x}'][session][2] 2520 * self.unknowns[sample2][f'session_D{self._4x}'][session][2] 2521 * np.array([[avg_D4x_1, avg_d4x_1, 1]]) 2522 @ CM 2523 @ np.array([[avg_D4x_2, avg_d4x_2, 1]]).T 2524 ) / a**2 2525 return float(c) 2526 2527 def sample_D4x_correl(self, sample1, sample2 = None): 2528 ''' 2529 Correlation between Δ4x errors of samples 2530 2531 Returns the error correlation between the average Δ4x values of two samples. 2532 ''' 2533 if sample2 is None or sample2 == sample1: 2534 return 1. 2535 return ( 2536 self.sample_D4x_covar(sample1, sample2) 2537 / self.unknowns[sample1][f'SE_D{self._4x}'] 2538 / self.unknowns[sample2][f'SE_D{self._4x}'] 2539 ) 2540 2541 def plot_single_session(self, 2542 session, 2543 kw_plot_anchors = dict(ls='None', marker='x', mec=(.75, 0, 0), mew = .75, ms = 4), 2544 kw_plot_unknowns = dict(ls='None', marker='x', mec=(0, 0, .75), mew = .75, ms = 4), 2545 kw_plot_anchor_avg = dict(ls='-', marker='None', color=(.75, 0, 0), lw = .75), 2546 kw_plot_unknown_avg = dict(ls='-', marker='None', color=(0, 0, .75), lw = .75), 2547 kw_contour_error = dict(colors = [[0, 0, 0]], alpha = .5, linewidths = 0.75), 2548 xylimits = 'free', # | 'constant' 2549 x_label = None, 2550 y_label = None, 2551 error_contour_interval = 'auto', 2552 fig = 'new', 2553 ): 2554 ''' 2555 Generate plot for a single session 2556 ''' 2557 if x_label is None: 2558 x_label = f'δ$_{{{self._4x}}}$ (‰)' 2559 if y_label is None: 2560 y_label = f'Δ$_{{{self._4x}}}$ (‰)' 2561 2562 out = _SessionPlot() 2563 anchors = [a for a in self.anchors if [r for r in self.sessions[session]['data'] if r['Sample'] == a]] 2564 unknowns = [u for u in self.unknowns if [r for r in self.sessions[session]['data'] if r['Sample'] == u]] 2565 2566 if fig == 'new': 2567 out.fig = ppl.figure(figsize = (6,6)) 2568 ppl.subplots_adjust(.1,.1,.9,.9) 2569 2570 out.anchor_analyses, = ppl.plot( 2571 [r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] in self.anchors], 2572 [r[f'D{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] in self.anchors], 2573 **kw_plot_anchors) 2574 out.unknown_analyses, = ppl.plot( 2575 [r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] in self.unknowns], 2576 [r[f'D{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] in self.unknowns], 2577 **kw_plot_unknowns) 2578 out.anchor_avg = ppl.plot( 2579 np.array([ np.array([ 2580 np.min([r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] == sample]) - 1, 2581 np.max([r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] == sample]) + 1 2582 ]) for sample in anchors]).T, 2583 np.array([ np.array([0, 0]) + self.Nominal_D4x[sample] for sample in anchors]).T, 2584 **kw_plot_anchor_avg) 2585 out.unknown_avg = ppl.plot( 2586 np.array([ np.array([ 2587 np.min([r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] == sample]) - 1, 2588 np.max([r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] == sample]) + 1 2589 ]) for sample in unknowns]).T, 2590 np.array([ np.array([0, 0]) + self.unknowns[sample][f'D{self._4x}'] for sample in unknowns]).T, 2591 **kw_plot_unknown_avg) 2592 if xylimits == 'constant': 2593 x = [r[f'd{self._4x}'] for r in self] 2594 y = [r[f'D{self._4x}'] for r in self] 2595 x1, x2, y1, y2 = np.min(x), np.max(x), np.min(y), np.max(y) 2596 w, h = x2-x1, y2-y1 2597 x1 -= w/20 2598 x2 += w/20 2599 y1 -= h/20 2600 y2 += h/20 2601 ppl.axis([x1, x2, y1, y2]) 2602 elif xylimits == 'free': 2603 x1, x2, y1, y2 = ppl.axis() 2604 else: 2605 x1, x2, y1, y2 = ppl.axis(xylimits) 2606 2607 if error_contour_interval != 'none': 2608 xi, yi = np.linspace(x1, x2), np.linspace(y1, y2) 2609 XI,YI = np.meshgrid(xi, yi) 2610 SI = np.array([[self.standardization_error(session, x, y) for x in xi] for y in yi]) 2611 if error_contour_interval == 'auto': 2612 rng = np.max(SI) - np.min(SI) 2613 if rng <= 0.01: 2614 cinterval = 0.001 2615 elif rng <= 0.03: 2616 cinterval = 0.004 2617 elif rng <= 0.1: 2618 cinterval = 0.01 2619 elif rng <= 0.3: 2620 cinterval = 0.03 2621 elif rng <= 1.: 2622 cinterval = 0.1 2623 else: 2624 cinterval = 0.5 2625 else: 2626 cinterval = error_contour_interval 2627 2628 cval = np.arange(np.ceil(SI.min() / .001) * .001, np.ceil(SI.max() / .001 + 1) * .001, cinterval) 2629 out.contour = ppl.contour(XI, YI, SI, cval, **kw_contour_error) 2630 out.clabel = ppl.clabel(out.contour) 2631 2632 ppl.xlabel(x_label) 2633 ppl.ylabel(y_label) 2634 ppl.title(session, weight = 'bold') 2635 ppl.grid(alpha = .2) 2636 out.ax = ppl.gca() 2637 2638 return out 2639 2640 def plot_residuals( 2641 self, 2642 hist = False, 2643 binwidth = 2/3, 2644 dir = 'output', 2645 filename = None, 2646 highlight = [], 2647 colors = None, 2648 figsize = None, 2649 ): 2650 ''' 2651 Plot residuals of each analysis as a function of time (actually, as a function of 2652 the order of analyses in the `D4xdata` object) 2653 2654 + `hist`: whether to add a histogram of residuals 2655 + `histbins`: specify bin edges for the histogram 2656 + `dir`: the directory in which to save the plot 2657 + `highlight`: a list of samples to highlight 2658 + `colors`: a dict of `{<sample>: <color>}` for all samples 2659 + `figsize`: (width, height) of figure 2660 ''' 2661 # Layout 2662 fig = ppl.figure(figsize = (8,4) if figsize is None else figsize) 2663 if hist: 2664 ppl.subplots_adjust(left = .08, bottom = .05, right = .98, top = .8, wspace = -0.72) 2665 ax1, ax2 = ppl.subplot(121), ppl.subplot(1,15,15) 2666 else: 2667 ppl.subplots_adjust(.08,.05,.78,.8) 2668 ax1 = ppl.subplot(111) 2669 2670 # Colors 2671 N = len(self.anchors) 2672 if colors is None: 2673 if len(highlight) > 0: 2674 Nh = len(highlight) 2675 if Nh == 1: 2676 colors = {highlight[0]: (0,0,0)} 2677 elif Nh == 3: 2678 colors = {a: c for a,c in zip(highlight, [(0,0,1), (1,0,0), (0,2/3,0)])} 2679 elif Nh == 4: 2680 colors = {a: c for a,c in zip(highlight, [(0,0,1), (1,0,0), (0,2/3,0), (.75,0,.75)])} 2681 else: 2682 colors = {a: hls_to_rgb(k/Nh, .4, 1) for k,a in enumerate(highlight)} 2683 else: 2684 if N == 3: 2685 colors = {a: c for a,c in zip(self.anchors, [(0,0,1), (1,0,0), (0,2/3,0)])} 2686 elif N == 4: 2687 colors = {a: c for a,c in zip(self.anchors, [(0,0,1), (1,0,0), (0,2/3,0), (.75,0,.75)])} 2688 else: 2689 colors = {a: hls_to_rgb(k/N, .4, 1) for k,a in enumerate(self.anchors)} 2690 2691 ppl.sca(ax1) 2692 2693 ppl.axhline(0, color = 'k', alpha = .25, lw = 0.75) 2694 2695 session = self[0]['Session'] 2696 x1 = 0 2697# ymax = np.max([1e3 * (r['D47'] - self.samples[r['Sample']]['D47']) for r in self]) 2698 x_sessions = {} 2699 one_or_more_singlets = False 2700 one_or_more_multiplets = False 2701 multiplets = set() 2702 for k,r in enumerate(self): 2703 if r['Session'] != session: 2704 x2 = k-1 2705 x_sessions[session] = (x1+x2)/2 2706 ppl.axvline(k - 0.5, color = 'k', lw = .5) 2707 session = r['Session'] 2708 x1 = k 2709 singlet = len(self.samples[r['Sample']]['data']) == 1 2710 if not singlet: 2711 multiplets.add(r['Sample']) 2712 if r['Sample'] in self.unknowns: 2713 if singlet: 2714 one_or_more_singlets = True 2715 else: 2716 one_or_more_multiplets = True 2717 kw = dict( 2718 marker = 'x' if singlet else '+', 2719 ms = 4 if singlet else 5, 2720 ls = 'None', 2721 mec = colors[r['Sample']] if r['Sample'] in colors else (0,0,0), 2722 mew = 1, 2723 alpha = 0.2 if singlet else 1, 2724 ) 2725 if highlight and r['Sample'] not in highlight: 2726 kw['alpha'] = 0.2 2727 ppl.plot(k, 1e3 * (r['D47'] - self.samples[r['Sample']]['D47']), **kw) 2728 x2 = k 2729 x_sessions[session] = (x1+x2)/2 2730 2731 ppl.axhspan(-self.repeatability['r_D47']*1000, self.repeatability['r_D47']*1000, color = 'k', alpha = .05, lw = 1) 2732 ppl.axhspan(-self.repeatability['r_D47']*1000*self.t95, self.repeatability['r_D47']*1000*self.t95, color = 'k', alpha = .05, lw = 1) 2733 if not hist: 2734 ppl.text(len(self), self.repeatability['r_D47']*1000, f" SD = {self.repeatability['r_D47']*1000:.1f} ppm", size = 9, alpha = 1, va = 'center') 2735 ppl.text(len(self), self.repeatability['r_D47']*1000*self.t95, f" 95% CL = ± {self.repeatability['r_D47']*1000*self.t95:.1f} ppm", size = 9, alpha = 1, va = 'center') 2736 2737 xmin, xmax, ymin, ymax = ppl.axis() 2738 for s in x_sessions: 2739 ppl.text( 2740 x_sessions[s], 2741 ymax +1, 2742 s, 2743 va = 'bottom', 2744 **( 2745 dict(ha = 'center') 2746 if len(self.sessions[s]['data']) > (0.15 * len(self)) 2747 else dict(ha = 'left', rotation = 45) 2748 ) 2749 ) 2750 2751 if hist: 2752 ppl.sca(ax2) 2753 2754 for s in colors: 2755 kw['marker'] = '+' 2756 kw['ms'] = 5 2757 kw['mec'] = colors[s] 2758 kw['label'] = s 2759 kw['alpha'] = 1 2760 ppl.plot([], [], **kw) 2761 2762 kw['mec'] = (0,0,0) 2763 2764 if one_or_more_singlets: 2765 kw['marker'] = 'x' 2766 kw['ms'] = 4 2767 kw['alpha'] = .2 2768 kw['label'] = 'other (N$\\,$=$\\,$1)' if one_or_more_multiplets else 'other' 2769 ppl.plot([], [], **kw) 2770 2771 if one_or_more_multiplets: 2772 kw['marker'] = '+' 2773 kw['ms'] = 4 2774 kw['alpha'] = 1 2775 kw['label'] = 'other (N$\\,$>$\\,$1)' if one_or_more_singlets else 'other' 2776 ppl.plot([], [], **kw) 2777 2778 if hist: 2779 leg = ppl.legend(loc = 'upper right', bbox_to_anchor = (1, 1), bbox_transform=fig.transFigure, borderaxespad = 1.5, fontsize = 9) 2780 else: 2781 leg = ppl.legend(loc = 'lower right', bbox_to_anchor = (1, 0), bbox_transform=fig.transFigure, borderaxespad = 1.5) 2782 leg.set_zorder(-1000) 2783 2784 ppl.sca(ax1) 2785 2786 ppl.ylabel('Δ$_{47}$ residuals (ppm)') 2787 ppl.xticks([]) 2788 ppl.axis([-1, len(self), None, None]) 2789 2790 if hist: 2791 ppl.sca(ax2) 2792 X = [1e3 * (r['D47'] - self.samples[r['Sample']]['D47']) for r in self if r['Sample'] in multiplets] 2793 ppl.hist( 2794 X, 2795 orientation = 'horizontal', 2796 histtype = 'stepfilled', 2797 ec = [.4]*3, 2798 fc = [.25]*3, 2799 alpha = .25, 2800 bins = np.linspace(-9e3*self.repeatability['r_D47'], 9e3*self.repeatability['r_D47'], int(18/binwidth+1)), 2801 ) 2802 ppl.axis([None, None, ymin, ymax]) 2803 ppl.text(0, 0, 2804 f" SD = {self.repeatability['r_D47']*1000:.1f} ppm\n 95% CL = ± {self.repeatability['r_D47']*1000*self.t95:.1f} ppm", 2805 size = 8, 2806 alpha = 1, 2807 va = 'center', 2808 ha = 'left', 2809 ) 2810 2811 ppl.xticks([]) 2812 ppl.yticks([]) 2813# ax2.spines['left'].set_visible(False) 2814 ax2.spines['right'].set_visible(False) 2815 ax2.spines['top'].set_visible(False) 2816 ax2.spines['bottom'].set_visible(False) 2817 2818 2819 if not os.path.exists(dir): 2820 os.makedirs(dir) 2821 if filename is None: 2822 return fig 2823 elif filename == '': 2824 filename = f'D{self._4x}_residuals.pdf' 2825 ppl.savefig(f'{dir}/{filename}') 2826 ppl.close(fig) 2827 2828 2829 def simulate(self, *args, **kwargs): 2830 ''' 2831 Legacy function with warning message pointing to `virtual_data()` 2832 ''' 2833 raise DeprecationWarning('D4xdata.simulate is deprecated and has been replaced by virtual_data()') 2834 2835 def plot_distribution_of_analyses( 2836 self, 2837 dir = 'output', 2838 filename = None, 2839 vs_time = False, 2840 figsize = (6,4), 2841 subplots_adjust = (0.02, 0.13, 0.85, 0.8), 2842 output = None, 2843 ): 2844 ''' 2845 Plot temporal distribution of all analyses in the data set. 2846 2847 **Parameters** 2848 2849 + `vs_time`: if `True`, plot as a function of `TimeTag` rather than sequentially. 2850 ''' 2851 2852 asamples = [s for s in self.anchors] 2853 usamples = [s for s in self.unknowns] 2854 if output is None or output == 'fig': 2855 fig = ppl.figure(figsize = figsize) 2856 ppl.subplots_adjust(*subplots_adjust) 2857 Xmin = min([r['TimeTag'] if vs_time else j for j,r in enumerate(self)]) 2858 Xmax = max([r['TimeTag'] if vs_time else j for j,r in enumerate(self)]) 2859 Xmax += (Xmax-Xmin)/40 2860 Xmin -= (Xmax-Xmin)/41 2861 for k, s in enumerate(asamples + usamples): 2862 if vs_time: 2863 X = [r['TimeTag'] for r in self if r['Sample'] == s] 2864 else: 2865 X = [x for x,r in enumerate(self) if r['Sample'] == s] 2866 Y = [-k for x in X] 2867 ppl.plot(X, Y, 'o', mec = None, mew = 0, mfc = 'b' if s in usamples else 'r', ms = 3, alpha = .75) 2868 ppl.axhline(-k, color = 'b' if s in usamples else 'r', lw = .5, alpha = .25) 2869 ppl.text(Xmax, -k, f' {s}', va = 'center', ha = 'left', size = 7, color = 'b' if s in usamples else 'r') 2870 ppl.axis([Xmin, Xmax, -k-1, 1]) 2871 ppl.xlabel('\ntime') 2872 ppl.gca().annotate('', 2873 xy = (0.6, -0.02), 2874 xycoords = 'axes fraction', 2875 xytext = (.4, -0.02), 2876 arrowprops = dict(arrowstyle = "->", color = 'k'), 2877 ) 2878 2879 2880 x2 = -1 2881 for session in self.sessions: 2882 x1 = min([r['TimeTag'] if vs_time else j for j,r in enumerate(self) if r['Session'] == session]) 2883 if vs_time: 2884 ppl.axvline(x1, color = 'k', lw = .75) 2885 if x2 > -1: 2886 if not vs_time: 2887 ppl.axvline((x1+x2)/2, color = 'k', lw = .75, alpha = .5) 2888 x2 = max([r['TimeTag'] if vs_time else j for j,r in enumerate(self) if r['Session'] == session]) 2889# from xlrd import xldate_as_datetime 2890# print(session, xldate_as_datetime(x1, 0), xldate_as_datetime(x2, 0)) 2891 if vs_time: 2892 ppl.axvline(x2, color = 'k', lw = .75) 2893 ppl.axvspan(x1,x2,color = 'k', zorder = -100, alpha = .15) 2894 ppl.text((x1+x2)/2, 1, f' {session}', ha = 'left', va = 'bottom', rotation = 45, size = 8) 2895 2896 ppl.xticks([]) 2897 ppl.yticks([]) 2898 2899 if output is None: 2900 if not os.path.exists(dir): 2901 os.makedirs(dir) 2902 if filename == None: 2903 filename = f'D{self._4x}_distribution_of_analyses.pdf' 2904 ppl.savefig(f'{dir}/{filename}') 2905 ppl.close(fig) 2906 elif output == 'ax': 2907 return ppl.gca() 2908 elif output == 'fig': 2909 return fig 2910 2911 2912class D47data(D4xdata): 2913 ''' 2914 Store and process data for a large set of Δ47 analyses, 2915 usually comprising more than one analytical session. 2916 ''' 2917 2918 Nominal_D4x = { 2919 'ETH-1': 0.2052, 2920 'ETH-2': 0.2085, 2921 'ETH-3': 0.6132, 2922 'ETH-4': 0.4511, 2923 'IAEA-C1': 0.3018, 2924 'IAEA-C2': 0.6409, 2925 'MERCK': 0.5135, 2926 } # I-CDES (Bernasconi et al., 2021) 2927 ''' 2928 Nominal Δ47 values assigned to the Δ47 anchor samples, used by 2929 `D47data.standardize()` to normalize unknown samples to an absolute Δ47 2930 reference frame. 2931 2932 By default equal to (after [Bernasconi et al. (2021)](https://doi.org/10.1029/2020GC009588)): 2933 ```py 2934 { 2935 'ETH-1' : 0.2052, 2936 'ETH-2' : 0.2085, 2937 'ETH-3' : 0.6132, 2938 'ETH-4' : 0.4511, 2939 'IAEA-C1' : 0.3018, 2940 'IAEA-C2' : 0.6409, 2941 'MERCK' : 0.5135, 2942 } 2943 ``` 2944 ''' 2945 2946 2947 @property 2948 def Nominal_D47(self): 2949 return self.Nominal_D4x 2950 2951 2952 @Nominal_D47.setter 2953 def Nominal_D47(self, new): 2954 self.Nominal_D4x = dict(**new) 2955 self.refresh() 2956 2957 2958 def __init__(self, l = [], **kwargs): 2959 ''' 2960 **Parameters:** same as `D4xdata.__init__()` 2961 ''' 2962 D4xdata.__init__(self, l = l, mass = '47', **kwargs) 2963 2964 2965 def D47fromTeq(self, fCo2eqD47 = 'petersen', priority = 'new'): 2966 ''' 2967 Find all samples for which `Teq` is specified, compute equilibrium Δ47 2968 value for that temperature, and add treat these samples as additional anchors. 2969 2970 **Parameters** 2971 2972 + `fCo2eqD47`: Which CO2 equilibrium law to use 2973 (`petersen`: [Petersen et al. (2019)](https://doi.org/10.1029/2018GC008127); 2974 `wang`: [Wang et al. (2019)](https://doi.org/10.1016/j.gca.2004.05.039)). 2975 + `priority`: if `replace`: forget old anchors and only use the new ones; 2976 if `new`: keep pre-existing anchors but update them in case of conflict 2977 between old and new Δ47 values; 2978 if `old`: keep pre-existing anchors but preserve their original Δ47 2979 values in case of conflict. 2980 ''' 2981 f = { 2982 'petersen': fCO2eqD47_Petersen, 2983 'wang': fCO2eqD47_Wang, 2984 }[fCo2eqD47] 2985 foo = {} 2986 for r in self: 2987 if 'Teq' in r: 2988 if r['Sample'] in foo: 2989 assert foo[r['Sample']] == f(r['Teq']), f'Different values of `Teq` provided for sample `{r["Sample"]}`.' 2990 else: 2991 foo[r['Sample']] = f(r['Teq']) 2992 else: 2993 assert r['Sample'] not in foo, f'`Teq` is inconsistently specified for sample `{r["Sample"]}`.' 2994 2995 if priority == 'replace': 2996 self.Nominal_D47 = {} 2997 for s in foo: 2998 if priority != 'old' or s not in self.Nominal_D47: 2999 self.Nominal_D47[s] = foo[s] 3000 3001 3002 3003 3004class D48data(D4xdata): 3005 ''' 3006 Store and process data for a large set of Δ48 analyses, 3007 usually comprising more than one analytical session. 3008 ''' 3009 3010 Nominal_D4x = { 3011 'ETH-1': 0.138, 3012 'ETH-2': 0.138, 3013 'ETH-3': 0.270, 3014 'ETH-4': 0.223, 3015 'GU-1': -0.419, 3016 } # (Fiebig et al., 2019, 2021) 3017 ''' 3018 Nominal Δ48 values assigned to the Δ48 anchor samples, used by 3019 `D48data.standardize()` to normalize unknown samples to an absolute Δ48 3020 reference frame. 3021 3022 By default equal to (after [Fiebig et al. (2019)](https://doi.org/10.1016/j.chemgeo.2019.05.019), 3023 Fiebig et al. (in press)): 3024 3025 ```py 3026 { 3027 'ETH-1' : 0.138, 3028 'ETH-2' : 0.138, 3029 'ETH-3' : 0.270, 3030 'ETH-4' : 0.223, 3031 'GU-1' : -0.419, 3032 } 3033 ``` 3034 ''' 3035 3036 3037 @property 3038 def Nominal_D48(self): 3039 return self.Nominal_D4x 3040 3041 3042 @Nominal_D48.setter 3043 def Nominal_D48(self, new): 3044 self.Nominal_D4x = dict(**new) 3045 self.refresh() 3046 3047 3048 def __init__(self, l = [], **kwargs): 3049 ''' 3050 **Parameters:** same as `D4xdata.__init__()` 3051 ''' 3052 D4xdata.__init__(self, l = l, mass = '48', **kwargs) 3053 3054 3055class _SessionPlot(): 3056 ''' 3057 Simple placeholder class 3058 ''' 3059 def __init__(self): 3060 pass >>>>>>> master
61def fCO2eqD47_Petersen(T): 62 ''' 63 CO2 equilibrium Δ47 value as a function of T (in degrees C) 64 according to [Petersen et al. (2019)](https://doi.org/10.1029/2018GC008127). 65 66 ''' 67 return float(_fCO2eqD47_Petersen(T)) =======63def fCO2eqD47_Petersen(T): 64 ''' 65 CO2 equilibrium Δ47 value as a function of T (in degrees C) 66 according to [Petersen et al. (2019)](https://doi.org/10.1029/2018GC008127). 67 68 ''' 69 return float(_fCO2eqD47_Petersen(T)) >>>>>>> masterCO2 equilibrium Δ47 value as a function of T (in degrees C) according to Petersen et al. (2019).
def fCO2eqD47_Wang(T):<<<<<<< HEAD72def fCO2eqD47_Wang(T): 73 ''' 74 CO2 equilibrium Δ47 value as a function of `T` (in degrees C) 75 according to [Wang et al. (2004)](https://doi.org/10.1016/j.gca.2004.05.039) 76 (supplementary data of [Dennis et al., 2011](https://doi.org/10.1016/j.gca.2011.09.025)). 77 ''' 78 return float(_fCO2eqD47_Wang(T)) =======74def fCO2eqD47_Wang(T): 75 ''' 76 CO2 equilibrium Δ47 value as a function of `T` (in degrees C) 77 according to [Wang et al. (2004)](https://doi.org/10.1016/j.gca.2004.05.039) 78 (supplementary data of [Dennis et al., 2011](https://doi.org/10.1016/j.gca.2011.09.025)). 79 ''' 80 return float(_fCO2eqD47_Wang(T)) >>>>>>> masterCO2 equilibrium Δ47 value as a function of
T(in degrees C) according to Wang et al. (2004) (supplementary data of Dennis et al., 2011).